Versions¶
The main changes in each release of the kavalai package. Releases are
tagged vX.Y.Z in the repository and published to PyPI.
Each release opens with a summary. Added, Changed, Removed and Fixed list the changes to the package. Upgrading lists what an existing deployment does to move to the release — migrations to run, names to change — and is the section to read first when upgrading.
1.0.7 — 2026-10-02¶
An agent node with chat history answers the current turn, and answers on
its last step. No migration.
Fixed¶
An agent node answered the previous turn. Each step sends the system prompt, the earlier turns and a step message; the earlier turns end on the previous user message, and the step message did not say what the current one was, so a model reading the conversation answered the turn before. The step message now opens with
[CURRENT REQUEST]and the run’s user message, as the chat history records it.Agent.promptandAgent.prompt_streamtakerequest; the engine passes the run’s, whichRunContextkeeps asuser_message(parallel branches included).An agent node could end without an output. A model that kept calling tools used every step and the run completed with nothing. The last step’s message now says it is the last and that tool calls will not be run, and tool calls returned on it are not run (they had no step left to be read in), so the model answers from what the earlier steps found. A run that still ends without an output is logged. Step templates receive
last_stepandrequest.
Upgrading¶
A custom
step_templateshould staterequestwhere the agent has chat history, and may uselast_step.
1.0.6 — 2026-09-29¶
The agent node remembers the conversation and sees only its declared
inputs. No migration.
Added¶
Chat history in agent nodes. The
agentnode takesuse_history(default on),history_limitandhistory_max_charsas thellmnode does, so a chatbot whose answer is produced by an agent loop remembers the conversation. The earlier turns are sent with every step, between the system prompt and the step message, and without the current user message — the node’spromptcarries it — sohistory_limitcounts the earlier messages only;AgentService.get_chat_historytakesexclude_run_idfor this. The agent’s intermediate steps are not written to the chat history.Agent.promptandAgent.prompt_streamtakehistory, a sequence ofChatMessage, andWorkflowBuilder.agentthe three node keys.
Changed¶
The agent’s prompt is two templates.
prompt_templaterenders the system message — the task, the context variables and the tool descriptions — and the newstep_template(default_step_template.j2) renders the user message that closes each step’s conversation: the planning data, the steps executed so far and the instruction to produce the next step, which were in the system prompt before. The system prompt is now the same on every step of a run. Both templates receive the same variables.
Fixed¶
An
agentnode listed the whole run context as its context variables — the parsed input and every earlier node’s output, a retrieved passage list included, rendered in full on every step, and twice when the prompt interpolated the same value. The node now hands the agent its resolvedinputsand nothing else, as thellmnode does; a tool call’sinput_argsname those inputs.Agent.promptandAgent.prompt_streamtake them asinputs, and list the run context’sdatawhen the argument is absent, as before.
Upgrading¶
An
agentnode in a session with earlier turns now sends them to the model. A node that should not — a research step whose session is reused for re-runs, say — setsuse_history: false.An
Agentbuilt with a customprompt_templatethat rendersstepskeeps working, but the defaultstep_templaterenders them too, so the model reads the step trace twice; move the step section into astep_templateof your own, or pass one that renders only the closing instruction.
1.0.5 — 2026-09-25¶
Every run records how long it took, the backoffice opens a project’s database read-only, and retrieval can be filtered by metadata. The release carries one migration in each set.
Added¶
Run duration.
runs.duration_seconds(agents revision 0006) records the wall-clock time of every workflow invocation, measured by the engine with a monotonic clock and written with the result, on success and on failure alike;WorkflowStatecarries it asduration_seconds, andAgentService.update_runtakes it.The backoffice shows it on each run of a conversation. The daily runtime chart sums it, falling back to
updated_at - created_atfor runs recorded before the column existed, which are not backfilled. The workflows page lays runs out in lanes by when they actually ended.
Read-only projects in the backoffice.
projects.read_only(backoffice revision 0004, on for every existing project) makes each connection the backoffice opens to a project’s agent database refuse writes at the database:default_transaction_read_only=onon PostgreSQL,PRAGMA query_onlyon SQLite.DatabaseManager.get_sessionmaker(read_only=True)andSqliteRagService(read_only=True)are new, and the RAG service the explorer builds no longer provisions on any project.projects.rag_schemanames the schema of a project’s RAG collections when a deployment keeps them apart from the runtime tables; empty means the agent schema, as before.
Metadata filtering in retrieval.
queryandquery_batchtakematch, a mapping of top-level metadata keys to string, number or boolean values, and return only the entries whose metadata has every one of them equal — the conditiondelete_by_metadataalready deleted by.batch_query_with_jointakes it too.The filter is a condition of the nearest-neighbour search itself, not applied to its result, so
top_kmatching entries come back when the collection holds that many: on PostgreSQL it ismetadata @> :matchunder the collection’s GIN index, with pgvector’s iterative index scan enabled as it is forsource_ids; on SQLite it joins theWHEREclause of the full scan.The
rag_querynode andWorkflowBuilder.rag_querycarry it asmatch. A string value is a template, and one that is exactly one{{ context.* }}placeholder takes the referenced value with its type, so a filter an earlier node extracted passes through unchanged. The rendered filter is recorded with the node’s inputs on its task row.
Documentation assistant. The documentation site carries a chat widget answering questions about Kaval.AI from the documentation, served by a Kaval.AI agent.
Fixed¶
kavalai[runtime]declaressqlalchemy[asyncio]. SQLAlchemy 2.1 no longer installsgreenletby default, so a fresh install of the extra resolved to a SQLAlchemy whose async engine failed to import. The base package still lists neither, and stays importable under Pyodide.
Upgrading¶
Run the agents migrations (
python -m kavalai.migrate_db agents) for revision0006, which adds the nullableruns.duration_secondsand rewrites nothing. Runs recorded before it keepNULLand are reported from their timestamps.Run the backoffice migrations (
python -m kavalai.migrate_db backoffice) for revision0004. Every existing project becomes read-only; the backoffice writes nothing to a project’s database, so nothing it does changes. A project’s RAG explorer no longer creates therag_collectionsregistry when it is missing — index a collection first.Browser (Pyodide) SQLite databases are recreated on first use, since
SQLITE_SCHEMA_VERSIONmoved to6.
1.0.4 — 2026-09-12¶
A release about serving an agent to the public. Outbound requests are guarded
against server-side request forgery, the stream a visitor sees can be filtered,
and a turn’s spend is bounded. The two RAG services share one storage model, a
SQLite file is accepted wherever a database URI is, the chat widget ships in
the wheel, and common is divided into four extras by weight. Test doubles
(kavalai.testing) and an HTML parser and chunker (kavalai.text)
join the base install.
Security¶
This release closes several ways in which an agent served to the public could be made to reach internal networks, to disclose what only its operator should see, or to spend without bound. This section names them; the detail is under the headings that follow.
Server-side request forgery.
http_requestandcrawl_urlrefuse loopback, private, link-local and cloud metadata targets.http_requestconnects to the address it checked, so DNS rebinding cannot redirect it. An agent meant to reach an intranet is built withallow_private_networks=True, an argument of the tool factory that the model cannot set. Seekavalai.netunder Serving and tools.Disclosure to end users.
kavalai.server.public_events()removes node events, token usage and restart reasons from a stream, and replaces a failed run’s error text — which can carry provider request ids, quota state or SQL — with a fixed message quoting the run id.Unbounded spend per turn.
max_output_tokenscaps what a call may generate,history_limitandhistory_max_charsbound the history anllmnode sends, andrun_timeoutbounds a run. A misspelt cap inllm_kwargsfails the load instead of being ignored.get_chat_historyreturned the oldest fifty messages of a session, so a long conversation paid for a full history on every turn while the model never saw the latest exchange.Fail-open retrieval filter.
source_ids=[]searched the whole collection; it now matches nothing, so a caller that derives the list from what a user may see, and derives an empty one, receives no entries.Least privilege.
provision=Falseon the RAG services issues no DDL, and read paths no longer create the registry, so the role an agent server connects as needs noCREATEright on the RAG schema.Personal data in the agent database.
record_payloads=Falsekeeps prompts, inputs, outputs and model-call payloads out of the task log, and the session deletes blank the model-call payloads of the conversations they remove, so a deleted conversation does not survive as a prompt inmodel_call_stats.Crafted input.
kavalai.textparses HTML and chunks text in linear time, without regular expressions, so a crafted page cannot stall the process that indexes it; its tests bound the time taken on adversarial input.
Added¶
Workflow engine¶
LLMNode.history_limit(default50) andhistory_max_chars: the chat history anllmnode sends is the most recent messages, and at most that many characters of them — whole messages are dropped from the oldest end.AgentService.get_chat_historytakesmax_chars.templates=onrun()andrun_stream(): values for templates the document declares, for one run. Rendering is one pass, so the values need no escaping; they are recorded underrun_templatesinruns.context.timeout=onrun/run_streamandrun_timeout=on the engine cancel a run after that many seconds, parallel branches included, and record it as failed with the newWorkflowTimeoutError. The agent server readsKAVALAI_AGENT_RUN_TIMEOUT_SECONDS.WorkflowEngine(record_context=False)keeps only a run’s input and output inruns.context.rag_querynodes takemin_similarity, and record their hits —id,source_id,similarity,metadata— on the task row and in theoutput_dataofnode_completed, whateverstoresays. The query embedding is reported to the run’s token accumulator.workflow_completedandworkflow_failedevents carryrun_id.
LLM clients¶
max_output_tokensonLlmClientParameters, sent under each provider’s own name:max_output_tokens(OpenAI, Gemini),max_tokens(Anthropic, WebLLM),options.num_predict(Ollama). On reasoning models the cap includes the reasoning tokens.KAVALAI_LLM_MAX_OUTPUT_TOKENSsets it for the agent server and the eval judge.OutputTruncatedError, raised when a provider stops at the output cap instead of the partial answer being returned. It is not retried, carries the partial output and token counts, and the truncated call is still recorded. It andLlmClientExceptionare exported fromkavalai.reasoning_effortreaches Gemini, asthinking_level, and Ollama, asthink("none"sendsfalse).dimensions=on the OpenAI and Gemini embedding clients. Bound at registration —register_embedding_provider("openai-512", OpenAIEmbeddingClient, dimensions=512)— it gives a reduced model a name of its own.
Retrieval¶
CollectionRagService(kavalai.rag.collections): the storage model the two RAG services share — arag_collectionsregistry and one table per collection — with the browse methods the backoffice needs (list_collections,get_stats,create_collection,drop_collection,get_embeddings_by_ids) on both backends.modelis optional on both constructors; without one a service browses existing collections.sqlite:///pathis accepted wherever a database URI is:KAVALAI_DB_URIfor the agent server and migrations,DatabaseManager.get_sessionmaker, andrag_service_from_uri(), the counterpart ofPostgresRagService.from_urithat picks the RAG service from the URI scheme and forwards further keyword options to it.delete_many(ids, collection_name=None)on every RAG service (one statement per collection on the SQL backends; a loop overdeleteby default), and, in the optional tier,delete_by_metadata(collection_name, match)andreplace(collection_name, texts, metadata_list, source_ids=None, *, match=None, source_id=None). Amatchis equality on top-level keys with scalar values, served bymetadata @> :matchon PostgreSQL.replaceembeds first, then deletes and inserts in one transaction, so a failure leaves the old rows; emptytextsdeletes the selection.min_similarity=onqueryandquery_batch.stats_receiver=onquery,query_batch,index,index_batchandreplace, and on the RAG service constructors as the default. Each embedding call’sModelCallStatgoes to it; arag_querynode passes the run’s receiver.provision=Falseon the RAG services: no DDL; a missing registry reads as empty and indexing into a missing collection raisesRuntimeErrornamingcreate_collection(). Collections are created beforehand withcreate_collection(name, dim, *, model=None, vector_type=None), which issues DDL whateverprovisionsays, andensure_registry()is public.vector_type="halfvec"onPostgresRagService(pgvector 0.7 or later): 16-bit embeddings and HNSW indexes for up to 4,000 dimensions. The type is read back from the collection’s column.Filtered PostgreSQL queries enable
hnsw.iterative_scan = relaxed_orderfor their transaction on pgvector 0.8 or later, so a filter whose rows lie away from the query still returnstop_krows.KAVALAI_RAG_MODELregisters thedefaultRAG service when the agent server starts, over the index atKAVALAI_RAG_URI(a Postgres URI orsqlite:///file) in the optionalKAVALAI_RAG_SCHEMA, with the normalizer fromKAVALAI_EMBEDDING_NORMALIZER_YAML; a deployment with one index needs no setup module. The URI is required with the model: the index is not assumed to live in the agent database.
Persistence and observability¶
model_call_stats.session_idandrun_id(agents migration0005): every model call a run makes — the query embedding of arag_querynode included — is attributed to its agent, session and run, so cost per run and per conversation is a query.TaskLogger.log_model_call,TokenAccumulator,StatsBridgeandAgentService.add_model_call_statstakesession_idandrun_id;get_model_call_statsfilters byagent_id,session_idandrun_id.MemoryTaskLogger.model_callsholdsModelCallRecordobjects carrying the ids, withmodel_calls_for_run;SqliteTaskLogger’s own table gains the columns andget_model_calls(run_id)filters by them.
record_nodesandrecord_payloadson every task logger: record no node rows, or keep names, timings, errors and token counts but not the prompts, inputs, outputs and model-call payloads.write_nodeandwrite_model_call, the public hooks a task logger backend implements, andTeeTaskLoggerto put a logger of one’s own — a meter, say — beside the database one.AgentService.list_sessions(agent_ids, *, search, external_id_prefix, exclude_external_id_prefix, start, end, limit, offset)— the backoffice list’s query, scoped to several agents at once — andAgentService.purge_sessions(before, agent_ids=None, batch_size=1000), which deletes idle sessions in batches, yields each batch’s ids, and blanks the purged sessions’ model-call payloads while keeping their token counts.migrate(set_name, connection=…)runs a migration set on an open connection, inside the caller’s transaction, andmigrate_asyncdoes the same from inside an event loop, with anAsyncConnectionor a URI.
Serving and tools¶
kavalai.server.public_events(), an event filter for streams served to people other than the operator (no node events, token usage or restart reasons; a failure’s text replaced by a fixed message quoting the run id), andevent_filter=onstream_sse_events,create_agent_routerandcreate_agent_app.KAVALAI_AGENT_PUBLIC_EVENTSapplies it underpython -m kavalai.server.kavalai.server.sse_responseandAgentRequest[InputT], so a route of a host’s own speaks the same SSE protocol as the router.kavalai.net, a guard against server-side request forgery for tools that fetch a URL the model chooses.is_public_address()andensure_public_url()refuse loopback, private, link-local, carrier-grade NAT, multicast, reserved and cloud metadata targets, and any IPv6 address that maps, translates or tunnels to such an IPv4 address.PublicOnlyTransportis an httpx transport that resolves a host once, checks every address and connects to the address it checked, so DNS rebinding cannot redirect it; each redirect hop is checked again.make_http_request(allow_private_networks=True)andmake_crawl_url(allow_private_networks=True)build the bundled tools for an agent meant to reach an intranet; the switch is an argument of the factory, not of the tool, so the model cannot set it.The chat widget ships in the wheel as
kavalai.widget(kavalai/widget/kaval-chatbot.jsand.css, moved fromchatbotwidget/);widget_dir()andasset_path(name)locate the installed files for a Python host, and Chat widget documents it. New options:textsfor every visible string, aria labels and error messages included, and a disclosure label,AI assistant, on by default (disclosure: falseremoves it).onFeedback(runId, vote, comment)with thumbs and an optional comment box, andwidget.on(...)events (open,close,reply,error,resize,feedback).agentConnectorheaders(an object or a function),onResponse,setConversationIdandstorage("session","local","none"); the reply carries therunIdofworkflow_started.--kcb-font-familyand--kcb-font-size.
Backoffice¶
SQLite for the backoffice.
KAVALAI_BO_DB_URIaccepts asqlite:///pathURI, the backoffice migration set applies to SQLite, and a project has adb_type—postgresql(default) orsqlite, in which case itsdb_nameis the agent database file. The project form and the projects page show the fields the chosen type needs.GET /projects/{project_id}/llm-call-statsacceptsagent_id,session_idandrun_id. The Model calls page links each call to its conversation and to its run’s tasks, filters by therun_idandsession_idquery parameters, and is reached from a conversation’s Model calls button. The task debugger lists the run’s model calls — tokens and duration per call — beneath its tasks.GET /projects/{project_id}/rag/collectionsreturns each collection’s name, model, dimension, schema version and entry count.
Packaging, testing and text¶
Four extras dividing
commonby weight:runtime(provider SDKs, MCP, FastAPI, asyncpg, sqlite-vector),webtools(crawl4ai),fastembedandbackoffice(Authlib, itsdangerous, scikit-learn, sse-starlette).commoninstalls all four and keeps its meaning.kavalai.testing, test doubles in the base install that replace the model and nothing else.ScriptedLlmClientanswers from a list of replies or a function, streams each reply through the base client’s streamer, retry and restart handling, reports aModelCallStatfor every call and records the requests it received; the instance is itself an engineclient_factory.FakeEmbeddingClientembeds any text deterministically by hashing its words, andfake_providers()registers both under a provider name for awithblock and restores the registries on exit.kavalai.text:parse_htmlreduces a page to its title, text blocks with their heading paths, links and robots directives in one pass, with the boilerplate rules as parameters;chunk_blocksandchunk_markdownpack blocks into chunks of abouttarget_chars(1200) and never overmax_chars(2000), splitting at lines, sentences and words before cutting. Standard library only, so it runs under Pyodide, and linear in its input.tools/zerocostchatbotuses it, andbuild_index.pygains--target-chars.
Changed¶
Workflow engine and LLM clients¶
An
llm_kwargskey that is not a field ofLlmClientParametersfails the workflow load, naming the key and listing the valid ones, with a hint towardsmax_output_tokensformax_tokens,max_completion_tokens,num_predictandmax_new_tokens; a value of the wrong type fails too. Such keys were ignored before.workflow.clients.build_parametersrefuses them as well.A
restartevent is emitted only by a node that streams; a node that streams nothing has no partial output for a client to discard.A run that fails with a
WorkflowExceptionis recorded as failed on its run row, as other failures already were.A RAG service registered by name is built once per engine and kept, instead of on every
rag_queryexecution.The OpenAI embedding client records the response’s
modelandusageonly; the vectors are no longer copied intomodel_call_stats.The OpenAI client sends a structured-output schema as
text.format, so the SDK no longer validates a truncated answer before the stream reports why it stopped. An OpenAI response that is incomplete for another reason, and an Anthropic stop atmodel_context_window_exceeded, raiseLlmClientException. An error a client raises as aRuntimeErrorsubclass reaches the stream consumer as that type.
Retrieval¶
source_ids=[]matches nothing:queryreturns[]andquery_batchone empty list per text, without embedding;Nonestill means no filter.index_batchwithsource_ids=[]and non-empty texts raises the length error. A query on a collection that does not exist returns[]without embedding.The registry’s model is authoritative: an existing collection is queried and indexed with the model recorded for it, one embedding client per model. The constructor’s
modelis the model for new collections; a mismatch logs a warning. A service built withmodel=Nonecan query and index existing collections and refuses only to create one.Read paths (
list_collections,get_stats,count_entries,query, the deletes) no longer create the registry.When two processes create one collection at once with different dimensions or models, the one whose registry row lost raises
ValueErrorinstead of carrying on.SqliteRagServiceuses the shared storage model: a registry and a table per collection, each with its own embedding dimension, instead of a singlerag_indextable with one dimension per file. A file in the old layout is refused with a message asking for the index to be rebuilt; thetable_nameconstructor argument is gone.collection_name=Nonenow means"default", as on Postgres, rather than every collection.examples/ragindex/index_csv.py --replaceusesreplace, one source per transaction.--indexinindex_csv.pyandquery_index.pyis a database URI or a SQLite file path; thepostgresshorthand, which readKAVALAI_DB_URI, is gone.
Persistence and observability¶
sessions.updated_atis moved by every run and so records a session’s last activity; before, nothing updated it after the session was created. Migration0005backfills it from each session’s last run, adds composite indexes —sessions(agent_id, external_id),sessions(agent_id, updated_at),sessions(updated_at),chat_messages(session_id, created_at),chat_messages(agent_id, created_at),model_call_stats(agent_id, created_at)— and drops the single-column indexes they replace.SQLITE_SCHEMA_VERSIONis5.A task logger’s
max_payload_bytescaps model-call payloads as well as node payloads.AgentService.delete_history_for_sessionalso blanks the session’s model-call payloads.kavalai.backoffice.sessions.get_sessions_summarydelegates tokavalai.agent_service.summarise_sessions(), whereSessionSummarynow lives.Migrations run over the runtime’s async drivers — asyncpg for
postgresql://, aiosqlite forsqlite://— through Alembic’srun_syncrecipe, instead of converting the URI to psycopg2. A URI that names a synchronous driver is run on that driver.migrate()called inside a running event loop runs on a worker thread. Itsschema,skip_create_schemaandmax_waitare keyword-only, and SQLite is no longer waited for.Backoffice migration
0002runs in batch mode, and UUID columns in the backoffice set useuuid_column(), so the set applies on SQLite as it does on Postgres.
Serving and tools¶
http_requestrefuses private, loopback, link-local and cloud metadata targets withUnsafeUrlError, and is a coroutine function. The guarded client ignoresHTTP_PROXY/HTTPS_PROXY; withuse_proxy=Trueonly the URL pre-check applies, since the proxy resolves the name.crawl_urlrefuses such a URL withsuccess=Falseand aRefused:message, and withholds the content when crawl4ai reports a final URL that fails the check.The widget no longer retries a turn the server has started: once an SSE frame has arrived, a broken stream ends with the
interruptedtext instead of sending the message again and paying for the run twice. Statuses below 500 are not retried either.
Backoffice¶
The conversation list is ordered by last activity — the time of a session’s most recent run — and labels its timestamp accordingly. It no longer depends on Postgres-only SQL (
DISTINCT ON,jsonb_typeof):json_typeof()andjson_array_length()render per dialect and a window function ranks the messages.POST /projects/{project_id}/rag/queryno longer requiresmodel: an existing collection is embedded with its recorded model, which the PCA projection uses as well. The RAG explorer shows the selected collection’s recorded model instead of asking for one, and drops All Collections, since a query searches exactly one collection.
Packaging¶
The agent image installs
kavalai[runtime](--build-arg EXTRAS=…for more) and the backoffice imagekavalai[runtime,backoffice].Install hints name the narrowest extra, e.g.
pip install "kavalai[runtime]"; the agent server, the backoffice, its embedding projector, FastEmbed and the crawl4ai tools fail with a hint instead of a bareImportError.
Removed¶
kavalai.llm_clients.kwargs_mapper, which nothing imported and which disagreed with the clients.psycopg2-binaryfrom every extra, andkavalai.migrate_db.ensure_sync_scheme.The RAG services no longer write embedding statistics into
model_call_statsthemselves. A standalone indexing job that wants the record passesstats_receiver=StatsBridge(task_logger, agent_id).PostgresRagService’sagentparameter.The private task logger hooks
_log_node_impland_log_model_call_impl(nowwrite_node/write_model_call), andkavalai.workflow.tasklog.postgres._to_orm_stat.chatbotwidget/at the repository root; the widget lives inkavalai/widget/.tools/zerocostchatbot/htmlmd.pyand the tool’s own chunker, replaced bykavalai.text.
Fixed¶
AgentService.get_chat_historyreturned the oldestlimitmessages of a session instead of the most recent ones, so a long conversation lost its latest turns from the model’s view.A response cut off at the output limit was returned as if complete: partial text, or for structured output JSON repaired into a model with content missing. Every client now checks the provider’s stop reason.
Gemini usage without a candidate token count (a cap spent on thoughts alone) no longer fails the stats record.
The wheel carries
default_prompt_template.j2;Agent()from an installed 1.0.x wheel raisedFileNotFoundError.sse-starlette, imported by the backoffice, is declared rather than arriving throughmcp; the deaddemo_agents/**package-data globs are gone.Widget styles: the button reset no longer overrides the send button, launcher, greeting and choice chips, and the
hiddenattribute is honoured, so the typing indicator and a closed window no longer show. Conversation ids are generated on pages without a secure context.A
normalizergiven to a RAG service was never applied: the services passed it to the embedding client without asking for normalisation. It is now applied on the index side and on the query side alike.SQLite
delete_by_metadatadoes not treat JSONtrueas the number 1, matching PostgreSQL.The Postgres insert casts embeddings to the schema-qualified
public.vectortype, as the query always did.
Upgrading¶
Migrations and stored data:
Run the agents migrations (
python -m kavalai.migrate_db agents) for revision0005. It rewritessessionsonce. On a large PostgreSQL database the indexes can be built beforehand withCREATE INDEX CONCURRENTLYunder the names above, and the revision then leaves them. Embedding vectors that earlier releases copied intomodel_call_stats.response_dataare not removed by the migration; clear them when convenient withupdate model_call_stats set response_data = null where call_type = 'embedding' and response_data is not null;.Browser (Pyodide) SQLite databases are recreated on first use, since
SQLITE_SCHEMA_VERSIONmoved to5.An index built under 1.0.3 by a RAG service that had a
normalizerholds unnormalised vectors, while its queries are now normalised. Rebuild such an index, or construct the service without the normalizer.
Workflows and code:
A workflow whose
llm_kwargscarries a key the clients do not know — most oftenmax_tokens— no longer loads. Rename it (max_output_tokens) or remove it; it had no effect before.An agent that must reach an intranet through
http_requestorcrawl_urlregistersmake_http_request(allow_private_networks=True)/make_crawl_url(allow_private_networks=True)instead. A direct synchronous call ofhttp_requestnow returns a coroutine.PostgresRagService(session_maker, model=None, *, normalizer=None, schema=None, provision=True, stats_receiver=None, vector_type="vector"): the positionalagentparameter is gone and every option aftermodelis keyword-only. Passnormalizer=by name; replaceagent=withstats_receiver=StatsBridge(task_logger, agent_id)where the statistics are wanted.model=on a RAG service now names the model for collections it creates. Existing collections are embedded with their recorded model, and a service without a model can query them, where it used to raise.A caller that computes
source_idsand may produce an empty list now gets no hits for it, rather than a search of the whole collection. PassNonewhere “no restriction” is meant.A task logger subclass overriding
_log_node_impl/_log_model_call_implimplementswrite_node/write_model_call(stats, *, agent_id, session_id, run_id)instead. Code that imported_to_orm_statto store statistics passes the PydanticModelCallStattoAgentService.add_model_call_statsor to aStatsBridge.
Installation:
Code that used psycopg2 because
kavalai[common]happened to install it — a sync engine on apostgresql://URI, for instance — must now depend onpsycopg2-binaryitself, or move to asyncpg.python -m kavalai.migrate_dbneeds neither change: it uses asyncpg, and an explicitpostgresql+psycopg2://URI still works where psycopg2 is installed.A service that installed
commononly to serve workflows can installkavalai[runtime]; addfastembedfor local embedding models andwebtoolsforcrawl_url/web_search.Pages that load the widget from
chatbotwidget/load it fromkavalai/widget/(or from the installed package, throughkavalai.widget.widget_dir()).
1.0.3 — 2026-08-28¶
Third-party providers register by name, the workflow engine gains rag_query
and parallel nodes and an explicit lifecycle, and kavalai.eval
grades a running agent. The packaging extras and several environment variables
are renamed, so read Changed and Removed before upgrading.
Added¶
Provider registries (
kavalai.llm_clients.registry):register_llm_provider(),register_embedding_provider()andregister_rag_service()accept a class, a dotted path or a callable, so a third-party client is one registration away.BaseLlmClient,BaseEmbeddingClientandensure_user_turnare exported for that purpose, andKAVALAI_PROVIDER_MODULESloads such modules at start-up.rag_queryworkflow node andWorkflowEngine(rag_services=…); a node resolves its service as node → graph → “default”.parallelworkflow node with concurrent branch execution.Engine lifecycle:
await engine.connect()/await engine.aclose(), or the async context manager, open and release the MCP sessions once per engine.Agent(allowed_tools=…)andallowed_toolsonagentnodes, with"*"andproto://server.*patterns.Fleet-wide model defaults:
WorkflowEngine(default_llm_model=…, default_llm_parameters=…), filled by the server fromKAVALAI_DEFAULT_LLM_MODELand theKAVALAI_LLM_*variables.Evaluation package
kavalai.eval(SimpleEvaluator,JudgeEvaluator, YAML case files) and thekavalai-evalconsole script.Six agent skills shipped in the wheel and installed by
kavalai-skills install.gpuextra (fastembed-gpu) for local embedding on an NVIDIA GPU.Key-free
web_searchtool over DuckDuckGo, and acrawl4aiCompose service.KAVALAI_AGENT_SETUP_MODULEregisters tools and RAG services before the agent server loads its workflow.Migrations:
model_call_statsrecordscached_prompt_tokensandreasoning_tokens;tasksrecordsseq,parent_task_nameandtool_uri;users.active_project_idis cleared when its project is deleted.Documentation: quickstart, architecture, comparison, serving, guides, reference, cookbook and deployment pages; examples
green_village,bakery,business_info_agent,ragindexandchat_client.
Changed¶
Breaking. Packaging extras collapsed to
common,common_web,gpu,testanddocs; the per-provider extras (openai,gemini,anthropic,ollama,rag,postgres,mcp,server,tools,all, …) are gone. Install withpip install "kavalai[common]".Breaking. Environment variables renamed:
GOOGLE_OAUTH_CLIENT_ID/_SECRET→KAVALAI_BO_GOOGLE_CLIENT_ID/_SECRET,FRONTEND_URL→KAVALAI_BO_FRONTEND_URL,BACKOFFICE_HOST/_PORT→KAVALAI_BO_HOST/_PORT,KAVALAI_LLM_TIMEOUT→KAVALAI_LLM_TIMEOUT_SECONDS,TOR_PROXY_HOST/_PORT→KAVALAI_TOR_PROXY_HOST/_PORT.KAVALAI_BO_SESSION_SECRET_KEYis required and has no fallback.create_model_call_stat(duration_sections=…)renamed toduration_seconds.The Gemini client retries only HTTP 429; 400, 401 and 403 raise immediately.
Integration tests carry the
integrationmarker and are deselected by default.
Removed¶
Breaking.
KAVALAI_OPENAI_SERVICE_TIER— useKAVALAI_LLM_SERVICE_TIERorllm_kwargs.kavalai.tools.websearch(Serper, LangSearch, Google Custom Search) and their API-key variables; the RSS tool.costandcurrencycolumns onmodel_call_stats(see Observability for the reason).KAVALAI_DEFAULT_EMBEDDING_MODEL.kavalai/tools/index_csv.pyandcli_chat.py— nowexamples/ragindexandexamples/chat_client.
Fixed¶
PostgresTaskLoggerdroppedcached_prompt_tokensandreasoning_tokens.A stale
active_project_idafter a project was deleted or a member removed made every project-scoped backoffice endpoint answer 403.The backoffice sessions page issued one query per session.
1.0.2 — 2026-08-11¶
Streaming execution, the Anthropic client and a SQLite RAG service, on a flattened package layout with Alembic migrations.
Added¶
AnthropicClient.SqliteRagService— a sqlite-vector file index that also runs in the browser — behind theBaseRagServiceinterface shared withPostgresRagService.Streaming:
WorkflowEngine.run_stream()yieldingWorkflowStreamEvent,POST /stream_agenton the agent server, per-nodestream_delta/stream_instructions/stream_partialsflags andstream_timeout_secondsonLlmClientParameters.Alembic migration sets
agentsandbackofficereplace the plain-SQL scripts.
Changed¶
Breaking. Package layout flattened:
kavalai.agents.*moved to the top level (kavalai.agent,kavalai.db,kavalai.server, …).Breaking.
LlmClientParametersno longer defaultstemperatureandtop_p; the provider’s defaults apply.All workflow persistence goes through
AgentService.
Removed¶
Breaking.
kavalai.workflow.storage(DataStorage,RunHandle,InMemoryDataStorage,SqliteDataStorage) and theRagServiceclass, replaced byAgentServiceand the RAG services above.
1.0.1 — 2026-07-06¶
First release on PyPI.
A minimal, Pyodide-compatible core with optional extras; Python 3.12 or later.
LLM clients for OpenAI, Gemini and Ollama behind one streaming interface, a FastEmbed embedding client, and
BrowserLLMClient(WebLLM) for execution in the browser.The planning agent and function kernel (Python, REST and MCP tools).
The workflow engine with
start,end,llm,agent,function,ifandswitchnodes, YAML definitions and SVG rendering.PostgreSQL/pgvector RAG service.
The agent REST server and the backoffice (FastAPI and Angular) with agents, conversations, RAG, workflow and task pages.
The documentation site.