Three distributions and the wire between them. Nothing here knows about genes, strategies or VEuPathDB: an assistant built on this runtime brings its own science.
| folder | distribution | import name |
|---|---|---|
packages/assistant-core/ |
assistant-core |
assistant_core |
packages/assistant-client-ts/ |
@veupathdb/assistant-client |
- |
packages/mcp-conformance/ |
veupathdb-mcp-conformance |
mcp_conformance |
This folder is its own Yarn project: package.json declares
packages/assistant-client-ts as its only workspace, pins the Yarn release, and
yarn.lock here is what the client resolves against. yarn install --immutable
is the first step of the client's CI lane, so the suite runs against the versions
the lock names rather than whatever a fresh install picks.
A consuming application names this repository, the workspace and one release tag
("@veupathdb/assistant-client": "git+https://github.com/VEuPathDB/ai-assistant-platform.git#workspace=@veupathdb/assistant-client&tag=v<version>").
Yarn clones the repository, installs it with its own lock, runs prepack and
packs dist, so the consumer compiles the built output and needs no install
here.
From ai 6.0.250 a resumed stream is seeded from empty state instead of from
the assistant message the client already holds, so the client rebuilds the
message the tail continues. DurableChatTransport therefore resumes a message
a turn left open from a cursor before that message's own start, drops what the
tail delivers before it, and opens the next tail itself where the host ended one
at a done. That is what section 6.1 of PROTOCOL.md asks of a turn suspended
on a durable task: the gap's data-task-progress and data-task-completed
chunks belong to the suspended turn's message, and a host serves them on a
second tail. The cursor a replay names comes from the store, which the snapshot
seeds from its openMessage, so a reload never tails from 0. The peer range
is >=6.0.250 <8, the releases that seed a resume that way;
tests/conformance/replayedMessage.test.ts is the gate, and it fails on
ai 6.0.154.
assistant-core owns conversations, messages, conversation_events,
memory_tombstones, chat_turn_cancellations, monthly_usage,
scratchpad_notes and scratchpad_compactions, and ships
the alembic history that creates them under src/assistant_core/alembic/,
recording its position in alembic_version_assistant_core. A host
application's chain uses its own version table, so the two share a database
without touching each other.
uv run python -m assistant_core.migrate # bring the runtime's tables to headThe runtime does not migrate at start. A host that embeds this package as a
library runs assistant_core.migrate.upgrade_head(connection) on its own
connection, after its own chain, because the runtime's tables name host tables
in foreign keys. A database whose host chain already created the tables a
revision would build needs no alembic stamp: each revision asks the inspector
first and records the position without rebuilding anything. A database holding
some of one revision's tables is refused, naming which are present and which
are missing.
assistant_core.migrate.OWNED_TABLES is the ten names, and
assistant_core.migrate.include_object is the alembic filter that keeps them. A
host whose own env.py maps its tables on the same declarative base uses that
filter's complement, so neither chain autogenerates a revision for the other's
tables. A revision names the tables it created in its own CREATES, which does
not move when the distribution grows; a test holds the union of those to
OWNED_TABLES.
The runtime declares one host table it does not own. A host supplies users
with a uuid id. Nothing else is read from it.
The runtime holds the rules that read its own rows and hands back the decisions a product makes. These seams carry that split.
assistant_core.quota counts spend into monthly_usage per user per
application per payer, and get_current(session, user_id, limit_usd=...) takes
the budget as an argument: the runtime stores no limit and reads no user record.
What a caller at a hundred percent is told is the host's. Every charge names
who paid, PaidBy.DEPLOYMENT or PaidBy.USER
(assistant_core.platform.types); the cap counts the deployment's spend only,
and get_period_totals(session, user_id, paid_by=PaidBy.USER) reads the spend
on the user's own key apart from it.
assistant_core.conversation.cancellation writes the stop row a running worker
polls, and fails the job of a worker that is already gone through
assistant_core.tasks.maintenance.
assistant_core.conversation.authz answers ownership over a
ConversationLookup, a protocol whose one member is get_by_id. A host passes
the thread store it already holds and inherits nothing from the runtime.
assistant_core.scratchpad.rendering.render_scratchpad draws a thread's note
index and appends a ScratchpadGuidance, three strings the host writes: what to
start noting on an empty scratchpad, what to do before the turn ends on a filled
one, and what is worth promoting to long-term memory.
build_scratchpad_toolset(promoted_kind=..., guidance=...) is a value a host
puts in an agent's toolsets; the runtime names no agent, the third string
lands on the promote_to_memory tool description, which is what the model
reads when it decides to promote, and promoted_kind is the memory kind a
promoted note is written under, because the kinds are the host's.
assistant_core.scratchpad.compactor.compact_scratchpad takes a factory that
builds the compactor agent, so the model and the rewriting instructions are the
host's, and a host builds one only when a ceiling is passed. The runtime owns
the gate, the token trim, the cost and the write-back.
assistant_core.tasks runs durable tools on a queue the host opens.
install_task_app(app, durable_queue=...) gives the runtime the procrastinate
application, its schema and the name of the queue durable jobs run on, which a
worker reads back from worker_queues(); install_worker_context(build) builds the turn context a durable body
reads; install_completion_turn(run) drives the turn a finished task opens;
install_durable_job_context(ctx) carries state a worker cannot re-derive, so
the runtime names no product's credential. Each of those has a reset_* in the
same module. A host subclasses DurableJobState and types every credential
CarriedSecret, and register_durable_jobs scrubs the carried state out of
the queue's own log lines, so no host filter matches on a product's key name.
declare_durable_tool names a tool once, and the decorator, the procrastinate
job and the worker body all read that value.
assistant_core.tasks.heartbeat.HeartbeatThread writes the beat that
worker_dead_heartbeat_seconds reads, and the settings refuse a beat too slow
for that window. assistant_core.tasks.names holds the queue and job
names a host wires, and assistant_core.tasks.chat_turn states the two fields
the stalled-job sweep reads out of a host's chat-turn payload.
assistant_core.registry.install_assistant_registry serves the assistants to
work that carries no request, such as the turn a finished durable task opens.
assistant_core.errors.AssistantCoreError is the base of every refusal the
runtime raises across that surface (ConversationNotFoundError,
ConversationForbiddenError, TurnStillRunningError). None of them names an
HTTP status; a host maps them onto its own transport.
assistant_core.platform.observability.install_observability(service_name=..., service_version=..., environment=..., include_content=...) exports the process's
traces over OTLP/HTTP to the endpoint the standard OTEL_EXPORTER_OTLP_ENDPOINT (or
_TRACES_ENDPOINT) names, with the headers OTEL_EXPORTER_OTLP_HEADERS names plus
any the host passes as exporter_headers; with no endpoint it installs nothing.
It instruments every agent run and both httpx distributions, and the SQLAlchemy
engine a host passes. Metrics export only when OTEL_EXPORTER_OTLP_METRICS_ENDPOINT
is set. A host runs each turn under traced(TraceScope(...)), one root span that
carries session.id (the conversation) and user.id on every span beneath it, and
reads the turn's id back with current_trace_id(). A durable body runs under a
root span of its own, named after its tool, on the same session. A priced model
request is exported with gen_ai.usage.cost, the price the runtime charged. An
agent that carries the ReasoningEffortOnSpan capability exports each model
request with gen_ai.request.reasoning_effort, the effort the request was sent
with: openai_reasoning_effort when the settings name it, else the thinking
level, and no attribute when neither names a level.
PROTOCOL.md is the
wire an assistant-core deployment serves and the TypeScript client reads: the frame grammar, cursor semantics, the
snapshot and tail contract, the turn shape, the chunk vocabulary and the
reduction rules. It is versioned and additive only.
Both sides are pinned to it. assistant-core's
tests/integration/conversation/test_protocol_document.py compares the document
against the chunks the runtime actually emits, so a new chunk kind fails there.
The client's suite is the consumer-side gate: yarn sync:protocol reads the
document into src/protocol/captured.json, and tests/conformance/ fails when
the capture and the document disagree. The sync is an authoring step; the gate
is the suite that reads the capture back. A change to PROTOCOL.md that neither
side implements fails both.
The document ships inside the runtime package, so an installed consumer reads it
at Path(assistant_core.__file__).parent / "PROTOCOL.md", the same bytes the
deployment serves. tests/packaging builds the wheel and reads it back.
What a host writes to serve that wire is embedding the runtime in a host: the runtime call behind each of the three endpoints, the order a chat handler runs, the installs a worker makes before its first job, and the refusals a host maps onto status codes.
mcp_conformance is the suite an MCP tool server passes before a deployment
admits it. It runs against a served endpoint
(pytest --pyargs mcp_conformance --mcp-endpoint <url> --mcp-bearer <token>)
and produces an admission record. It ships apart from the runtime because a
deployment reads a server it did not build.
yarn install --immutable
cd packages/assistant-core && uv sync --frozen && uv run ruff check src tests && uv run ruff format --check src tests && uv run mypy --strict src && uv run pytest && uv run pytest tests/packaging -m wheel --override-ini addopts=''
cd packages/assistant-client-ts && yarn typecheck && yarn lint && yarn format:check && yarn test && yarn build
cd packages/mcp-conformance && uv sync --frozen && uv run ruff check src tests && uv run mypy --strict src && uv run pytest
node scripts/check-knowledge.mjs && node --test scripts/check-knowledge.test.mjsThose four lanes are what .github/workflows/ci.yml runs, command for command.
.pre-commit-config.yaml carries the same checks as hooks, and leaves the wheel
check and the integration half of the runtime suite to CI.
assistant-core's suite runs with no application installed; that is what
makes the boundary an installation fact rather than a lint rule.
docs/knowledge/ holds the choices behind this
repository, in Open Knowledge Format v0.2. scripts/check-knowledge.mjs is its
gate: every page carries a type, every relative link resolves, and every page
is linked from its directory's index.