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
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file matilda_agent_sdk-0.1.0.tar.gz.
File metadata
- Download URL: matilda_agent_sdk-0.1.0.tar.gz
- Upload date:
- Size: 25.9 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via:
twine/6.1.0 CPython/3.13.7
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
da857604de5a981ef548ec160da4d429a26881d936b3af31c95bb52435dbd460
|
|
| MD5 |
0d99960b6b2e89fdb7e48bd6131fa16c
|
|
| BLAKE2b-256 |
43dc886edba01ef4964a0ecf1d17c8eac176c4d98ca18a2deb7d6ce3f90b358e
|
Provenance
The following attestation bundles were made for matilda_agent_sdk-0.1.0.tar.gz:
Publisher:
publish-pypi.yml on MaincodeHQ/matilda-core
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
matilda_agent_sdk-0.1.0.tar.gz -
Subject digest:
da857604de5a981ef548ec160da4d429a26881d936b3af31c95bb52435dbd460 - Sigstore transparency entry: 2713178419
- Sigstore integration time:
-
Permalink:
MaincodeHQ/matilda-core@f9015a4f06a6d32ecba95da8ee1967b53d79cd88 -
Branch / Tag:
refs/heads/main - Owner: https://github.com/MaincodeHQ
-
Access:
private
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
self-hosted -
Publication workflow:
publish-pypi.yml@f9015a4f06a6d32ecba95da8ee1967b53d79cd88 -
Trigger Event:
workflow_dispatch
-
Statement type:
File details
Details for the file matilda_agent_sdk-0.1.0-py3-none-any.whl.
File metadata
- Download URL: matilda_agent_sdk-0.1.0-py3-none-any.whl
- Upload date:
- Size: 24.1 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via:
twine/6.1.0 CPython/3.13.7
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
3764d8d2f4464e97753afb7aa2794f9f8ba2d0038edfe21bf6cb67697bc9dbd8
|
|
| MD5 |
446d8f98e51bfbc556f6d1edab710721
|
|
| BLAKE2b-256 |
a602a1a3811392807a413bf82125225e1575c0866fcb0320390c2727292062f4
|
Provenance
The following attestation bundles were made for matilda_agent_sdk-0.1.0-py3-none-any.whl:
Publisher:
publish-pypi.yml on MaincodeHQ/matilda-core
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
matilda_agent_sdk-0.1.0-py3-none-any.whl -
Subject digest:
3764d8d2f4464e97753afb7aa2794f9f8ba2d0038edfe21bf6cb67697bc9dbd8 - Sigstore transparency entry: 2713178479
- Sigstore integration time:
-
Permalink:
MaincodeHQ/matilda-core@f9015a4f06a6d32ecba95da8ee1967b53d79cd88 -
Branch / Tag:
refs/heads/main - Owner: https://github.com/MaincodeHQ
-
Access:
private
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
self-hosted -
Publication workflow:
publish-pypi.yml@f9015a4f06a6d32ecba95da8ee1967b53d79cd88 -
Trigger Event:
workflow_dispatch
-
Statement type: