Skip to main content

flintd for Python

The Python client of a flintd daemon: the Tools an agent writes for itself, in the shape each provider asks for. It is remote-only — a URL and a token — and the client itself needs nothing but the standard library.

uv add flintd                    # the client
uv add "flintd[pydantic-ai]"     # and the Pydantic AI toolset

Connect

Start the daemon with flintd serve. It logs the address and the path of its token file.

from pathlib import Path
from flintd import Flint

flint = Flint("http://127.0.0.1:3546", (Path.home() / ".flintd" / "token").read_text().strip())

flint.status()  # what this Tenant holds
flint.tools()  # the meta tools and the Active Tools
flint.call("tool_find", {"query": "count words"})
flint.call("word_count", {"text": "one two"})

Every method answers the daemon's own payload, and every refusal is a ToolError carrying the daemon's stable code, its message and its details. A code this build does not know reads as internal_error. A daemon that cannot be reached, or that did not answer in time, raises TransportError, a ToolError whose code is transport_failed — the one code no daemon ever sends, so a refusal of a Tool is never confused with a daemon that is not there. Nothing was run and no call was recorded.

Method What it does
start() answers when the daemon answers; a client starts no daemon
stop() does nothing: a daemon belongs to whoever started it
status(), library(), tools(format=None) what the Tenant holds
call(name, args, meta) the result alone
call_with_id(name, args, meta) {"id", "result"}; id is None for a meta tool
report(call_id, outcome, note) "positive" or "negative" against one recorded call
find(query, limit) the Tools closest to a query
approvals(), approve(id, note), deny(id, note) the Manifest decisions
connections(), add_connection(...), remove_connection(name) the credentials the Proxy attaches
call_from(block, format, meta) run a provider's tool-call block and answer in its tool-result shape

This client registers no watcher: it has no onChange and no onApproval, which the TypeScript client answers without either crossing the wire. Read tools() again to see the list after a write, and approvals() to see every request waiting, including the ones another client raised.

The formats

tools(format) answers the tools of one provider's request. A meta tool keeps its own name and every Tool of the Library carries fl_; a stored name is at most 60 characters, so fl_ still fits the 64 every provider allows.

format What it answers
None the daemon's own list: {name, description, parameters}
"anthropic" {name, description, input_schema}
"openai" the Responses API: {type, name, description, parameters, strict}
"openai-chat" Chat Completions: {type, function: {name, description, parameters, strict}}
"gemini" the functionDeclarations of one tools entry: {name, description, parameters}, and response when the Tool declares a result schema, all in Gemini's schema subset

strict is true only for a closed object schema whose every property is required, nested no deeper than ten levels, with no node typed null and no node carrying minLength, maxLength, title, default or examples — which is what OpenAI's structured outputs holds a strict tool to. The Gemini schema carries no additionalProperties and no examples; it writes minItems, maxItems, minLength and maxLength as strings, the way proto JSON writes an int64, and it moves an enum that is not a list of strings on a string into the description, because a genai Schema.enum is string[].

This emitter and the TypeScript one answer the same shapes: tests/parity.json holds one tool list and the answer both must give, and tests/test_parity.py and packages/core/test/format-parity.test.ts each assert their own side of it.

The TypeScript SDK emits two more, vercel and mcp; this client does not, and asking for one is invalid_arguments. Reach them through @flintd/sdk, or through the daemon's own /mcp surface, which every MCP client speaks already.

call_from takes the block the model emitted and answers the block the provider wants back:

answered = flint.call_from(block, "anthropic")  # {"type": "tool_result", "tool_use_id": ..., "content": ...}
messages.append({"role": "user", "content": [answered]})

A refusal goes into the provider's own error slot — is_error for Anthropic, the output string for OpenAI, the response.error object for Gemini — so the model reads the refusal and acts on it rather than the run ending.

Pydantic AI

from pydantic_ai import Agent
from flintd.toolset import FlintToolset

agent = Agent("anthropic:claude-sonnet-5", toolsets=[FlintToolset(flint)])

The toolset reads the Library again at every run step, so a Tool the model writes with tool_create in one step is callable as fl_<name> in a later one, once it has earned Active. A refusal reaches the model as a retry carrying "<code>: <message>". Each call carries harness: "pydantic-ai" and the run id as its session.

Approvals

A Tool whose Manifest asks for a file root, a host, a Connection or exec runs nothing until a person grants it. A call to one that is waiting is refused with awaiting_approval, and the decision is two calls:

waiting = [one for one in flint.approvals() if one["status"] == "pending"]
flint.approve(waiting[0]["id"], "the host is ours")

flintd approvals list and flintd approvals approve <tool> are the same decision on the command line.

Develop

uv run pytest        # against a daemon this suite starts, with no key and no network beyond 127.0.0.1
uv run ruff check
uv run ruff format --check
uv run mypy --strict src tests examples

Release files for flintd 0.1.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for flintd 0.1.0
File Size Uploaded
flintd-0.1.0.tar.gz 63.3 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for flintd 0.1.0
File Interpreter ABI Platform
flintd-0.1.0-py3-none-any.whl Python 3 none any Details

Total release size: 79.2 kB

Release files / flintd-0.1.0.tar.gz

Download URL flintd-0.1.0.tar.gz
Size 63.3 kB
Tags Source
SHA-256 checksum
How to use checksums
e7ea5a32d3dc7e6886b827405788b923199c6f7b3598575271e0e007db707f23
BLAKE2b-256 checksum
How to use checksums
df861159a9441d9b8e26ea59ef803e235dd07955aebe189efcf6fbea3341978e
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via uv/0.12.15 {"installer":{"name":"uv","version":"0.12.15","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}

Release files / flintd-0.1.0-py3-none-any.whl

Download URL flintd-0.1.0-py3-none-any.whl
Size 15.8 kB
Tags Python 3
SHA-256 checksum
How to use checksums
79855981843fbdb4457024e2ad8a8a7ce52e4068366c139c3af5d3bbf31dad02
BLAKE2b-256 checksum
How to use checksums
dcc7d0f28939c18a1640f49e5f06c796511234302a5556a98f10a7cee10313ea
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via uv/0.12.15 {"installer":{"name":"uv","version":"0.12.15","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}

Release history Release notifications | RSS feed

This release

0.1.0 This release

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page