matilda-agent-sdk
Python port of @maincode-ai/matilda-agent-sdk. Layered on top of
matilda-client, the public
Python SDK for the Matilda API — adds agent lifecycle, streaming, client
tool execution with human-in-the-loop composition, and multi-agent
pipelines.
Async-first. Dependencies: matilda-client + httpx (pulled in transitively).
Install
pip install matilda-agent-sdk
Requires Python 3.12+.
Quick start
import asyncio
from matilda_agent_sdk import Agent, AgentAuth, MatildaClient, Runner
async def main():
async with MatildaClient() as client:
await AgentAuth(client).login_with_device_flow(client_id="matilda-code")
runner = Runner(client)
result = await runner.run(
Agent(name="reviewer", instructions="Be terse."),
"Summarise the latest commit.",
)
print(result.final_output)
asyncio.run(main())
Agents
An Agent is a named, purposed system prompt. purpose selects the model
routing and response mode — code (default) gets the routing-intent block
and responseMode "auto"; analysis maps to responseMode "deep".
Agent(
name="summariser",
purpose="general",
instructions="You write tight, high-signal summaries.",
context="Assume the reader has no prior context.",
metadata={"owner": "growth", "tier": "foundational"},
)
instructions may be a string or a sync/async callable of a context
dict (agent_name, input, purpose, metadata) resolved per turn. Name
is required and trimmed; a blank name raises ValueError. The effective
handler merges the callable result with context (joined as Context: on a
new line).
Running agents
At its simplest, Runner.run() blocks until the turn finishes:
from matilda_agent_sdk import Agent, Runner, configure_default_client
runner = Runner() # default: bare MatildaClient; override with configure_default_client(...)
result = await runner.run(
Agent(name="researcher", purpose="analysis"),
"What changed in v1.1.0 of the SDK?",
)
print(result.final_output) # str
print(result.usage) # UsageSummary(output_tokens=182, context_pct=None, ...)
print(result.errors) # [StreamErrorDetail(...)] — only if the stream errored and you swallowed it
Runner.stream() is the async-generator variant — yields every agent event
(see below), still raises on stream errors unless you opt out.
Structured output
run_object() / stream_object() feed a JSON schema (dict or pydantic
model class — pydantic is not a dependency) into the server-side grammar
compiler and validate the accumulated text before returning:
schema = {
"type": "object",
"properties": {
"name": {"type": "string"},
"age": {"type": "integer"},
},
"required": ["name"],
}
obj = await runner.run_object(
agent,
"Extract a person from 'Ada, 36'",
schema=schema,
)
print(obj.object) # {'name': 'Ada', 'age': 36}
run_object raises MatildaObjectParseError if the model's text is not
valid JSON or fails schema validation (local $ref/$defs are inlined
first since the server cannot resolve pointers).
Client tools (human-in-the-loop)
Declare a tool shape once and hand the handler a ToolHandlers dict; the
agent may then request a tool execution mid-turn and you decide what to
allow:
from matilda_agent_sdk import ClientToolRequested, ToolHandlers
async def confirm(args, ctx):
# Async prompt the human — `ctx.tool_call_id` ties this request to the stream.
approved = await asyncio.to_thread(input, f"run {args['command']}? [y/n] ")
if approved.strip().lower() != "y":
return ToolResult(
content="Human declined the tool execution.",
is_error=True,
)
return ToolResult(content="approved")
async def git_status(args, ctx):
if args.get("command", "").strip() != "git status":
return ToolResult(content="Only `git status` is allowed.", is_error=True)
proc = await asyncio.create_subprocess_exec("git", "status", "--porcelain", stdout=PIPE)
out, _ = await proc.communicate()
return ToolResult(content=out.decode() or "(clean working tree)")
handlers: ToolHandlers = {
"confirm": confirm,
"git_status": git_status,
}
result = await runner.run(
Agent(name="triage", purpose="code"),
"Check the working tree state before committing.",
client_tools=[
{
"name": "git_status",
"description": "Show the git working-tree state.",
"parameters": {"type": "object", "properties": {"command": {"type": "string"}},
"required": ["command"]},
},
],
tool_handlers=handlers,
)
Tool roundtrips are capped (max_tool_roundtrips, default 25) so a buggy
model cannot loop forever; the cap is enforced client-side and the runner
raises MatildaAgentStreamError when it trips.
The DSML interceptor rewires <|DSML|tool_call> text tokens into real
tool events — the model sometimes emits tool calls as literal text, and
the interceptor terminates, parses, and re-emits them as first-class
events (ClientToolRequested etc.).
Streaming text
For a pure text stream (e.g. piping to stdout), stream_text() yields
deltas and raises on the safety line:
async for delta in runner.stream_text(agent, "Draft the release notes."):
print(delta, end="")
run_text() is the same idea for a single string, and raises
SafetyReplaceError when the server replaces in-flight output.
Sessions (multi-turn threads)
A Session threads turns together with a single conversation_id and
replays the accumulated transcript ahead of each turn — the server's agent
traffic is persist: false, so mentioning "the previous turn" only works
when the client resends the history:
from matilda_agent_sdk import create_session
session = create_session(agent) # or Session(agent)
await session.run("Draft a 1-line commit message.")
reply = await session.run("Now make it sound more like a human wrote it.")
print(session.turns[-1].final_output)
Session exposes conversation_id, turns, and last_turn for iteration
or persistence. Pass a runner=/client= pair to bind it to a specific
client config; otherwise it uses the shared default runner.
Parallel fan-out
import asyncio
from matilda_agent_sdk import run_text
results = await asyncio.gather(
run_text({"name": "bug-triage"}, "Triage crash #1234"),
run_text({"name": "bug-triage"}, "Triage crash #1235"),
run_text({"name": "bug-triage"}, "Triage crash #1236"),
)
For load or auth isolation, give each runner its own client:
runner_a = Runner(client_a)
runner_b = Runner(client_b)
await asyncio.gather(runner_a.run(...), runner_b.run(...))
Runner(client) stamps agent provenance per request on its own traffic
only — the supplied client (and shared singleton) is never mutated, so
parallel runners and later direct client calls each report the right SDK.
Module-level conveniences
Once you've installed a client into the default runner (via
configure_default_client(...)), the module-level functions mirror the TS
SDK:
from matilda_agent_sdk import configure_default_client, run, run_text, run_object, stream_text
configure_default_client(MatildaClient(token="eyJ..."))
result = await run({"name": "informer"}, "Status?")
configure_default_client(client) installs the default once; subsequent
calls reuse it unless you reset the process.
Auth
The SDK reuses the shared client for auth — a Runner with no client
materialises a bare MatildaClient. For managed login, use
AgentAuth (loopback PKCE / device flow):
from matilda_agent_sdk import AgentAuth, Runner
token_manager = await AgentAuth().login_with_device_flow(client_id="matilda-code")
AgentAuth snapshots the client's previous get_token provider before
installing the TokenManager, and restores it on logout() — the client's
auth state is never blanked when it is shared with other callers.
See the matilda-client
README
for token stores, persistence, and matilda-key.
Client resources
Runner proxies the underlying client resources so agent code doesn't
have to hold a separate client reference: runner.files,
runner.conversations, and runner.feedback map directly onto
client.files, client.conversations, and client.feedback (the
agent-layer FeedbackResource stamps the agent SDK as the reporting
package on report_bug).
await runner.files.upload("dataset.jsonl")
await runner.conversations.list(limit=20)
await runner.feedback.report_bug(title="SDK crash on resume", description="...")
Resume a detached stream
resume_agent_stream(stream_id, last_event_id=...) replays a detached
stream from the cursor and accumulates it into a AgentRunResult — the
same shape the normal run() returns. It is resilient to a 401 by
force-refreshing the managed token once before giving up.
result = await resume_agent_stream("str_...", last_event_id="ev_42")
print(result.final_output)
Errors
MatildaError— base.MatildaAgentRunError— hard HTTP failure surfaced by the agent path.MatildaAgentStreamError(result)— the run finished in an errored stream state;resultcarries the accumulated events, errors, and partial output.MatildaObjectParseError— structured-output text was not valid JSON or failed the provided schema.SafetyReplaceError— the server replaced in-flight output (only raised byrun_text/stream_text/run_object; onrun/streamit's recorded on the result'ssafety_replacefield).
Development
python -m venv .venv && .venv/bin/pip install -e ".[dev]"
.venv/bin/python -m pytest tests
.venv/bin/ruff check src tests
Metadata
Release files for matilda-agent-sdk 0.1.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | Provenance | Actions |
|---|---|---|---|---|
| matilda_agent_sdk-0.1.0.tar.gz | 25.9 kB | With provenance | Details for matilda_agent_sdk-0.1.0.tar.gz |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Actions Reset |
|---|---|---|---|---|
| matilda_agent_sdk-0.1.0-py3-none-any.whl | Python 3 | none | any | Details for matilda_agent_sdk-0.1.0-py3-none-any.whl |
Total release size: 49.9 kB
Release files / matilda_agent_sdk-0.1.0.tar.gz
| Download URL | matilda_agent_sdk-0.1.0.tar.gz |
|---|---|
| Size | 25.9 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
da857604de5a981ef548ec160da4d429a26881d936b3af31c95bb52435dbd460
|
|
BLAKE2b-256 checksum How to use checksums |
43dc886edba01ef4964a0ecf1d17c8eac176c4d98ca18a2deb7d6ce3f90b358e
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/6.1.0 CPython/3.13.7
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Sep 4, 2026.
Transparency logRelease files / matilda_agent_sdk-0.1.0-py3-none-any.whl
| Download URL | matilda_agent_sdk-0.1.0-py3-none-any.whl |
|---|---|
| Size | 24.1 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
3764d8d2f4464e97753afb7aa2794f9f8ba2d0038edfe21bf6cb67697bc9dbd8
|
|
BLAKE2b-256 checksum How to use checksums |
a602a1a3811392807a413bf82125225e1575c0866fcb0320390c2727292062f4
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/6.1.0 CPython/3.13.7
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Sep 4, 2026.
Transparency logSecurity for matilda-agent-sdk 0.1.0
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
Full provenance
All 2 files in this release have been uploaded with provenance metadata.
Attestation for matilda_agent_sdk-0.1.0.tar.gz
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Sep 4, 2026.
Transparency logA note on package security
PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.
To learn more about protecting yourself from malicious actors, visit our package security documentation.