Microsoft Agent Framework¶
SelfLearningProvider is a Microsoft Agent Framework (MAF) context provider.
Attach it to an agent and every run recalls learned skills and memories first and records the episode afterwards.
Your agent code does not change.
Install¶
The maf extra installs agent-framework-core. Install the MAF chat client package you use separately, for example
agent-framework-anthropic or agent-framework-openai.
Minimal example¶
from agent_framework import Agent
from myelinate.adapters.maf import SelfLearningProvider
from myelinate.client import GatewayClient
learner = SelfLearningProvider(GatewayClient(), agent_id="support-bot")
agent = Agent(chat_client, instructions="You are a support agent.", context_providers=[learner])
session = agent.create_session()
session.state["myelinate_user_id"] = "user-42" # optional: per-user memories and preferences
result = await agent.run("I want a refund for order 5521", session=session)
await learner.flush() # before shutdown: wait for background uploads
A complete, runnable version is in examples/maf_agent.py.
It uses MAF's AnthropicClient when ANTHROPIC_API_KEY is set and agent-framework-anthropic is installed, and
otherwise a scripted stand-in client so it runs without a model.
What the provider does¶
Before each run (before_run):
- Joins the text of the run's input messages into a query. Runs with no text input are skipped.
- Calls recall with
agent_id, the user id (see below) and the MAFsession.session_id. - Adds the returned
instructionsto the run withcontext.extend_instructions(...). - If
add_tools=True(the default), adds two tools for this run:skill_view(name)loads the full text of a learned skill listed under "Other learned skills".remember(content, kind="fact")saves a durable memory.kindisfact,preferenceorlesson.
After each run (after_run):
- Collects input and response messages and the function calls/results in the response.
- Determines the outcome:
outcome_fn(context)if given; otherwiseunknownwhen there is a response andfailurewhen there is none. - Attaches
verify_fn(context)if given. - Credits any skill the model loaded with
skill_viewduring the run. - Uploads the episode in a background task, so the response is not delayed. The resulting episode id is stored in
the session state under the provider's
source_id:session.state["myelinate"]["last_episode_id"].
Note
If agent.run(...) raises (for example the model call fails), MAF does not call after_run, so no episode is
recorded for that run. Use outcome_fn / verify_fn for failures that still produce a response.
Constructor¶
SelfLearningProvider(
client, # GatewayClient or EmbeddedClient
agent_id="default", # knowledge namespace
*,
user_id=None, # fallback user id when the session doesn't set one
source_id="myelinate", # MAF provider id; also the session.state key for provider state
add_tools=True, # add skill_view and remember
outcome_fn=None, # Callable[[SessionContext], Outcome]
verify_fn=None, # Callable[[SessionContext], Verification | None]
fail_open=True, # False: a recall failure raises instead of running without learned context
)
GatewayClient or EmbeddedClient¶
Both implement the same LearnerClient protocol, so the provider works with either.
GatewayClient (recommended)¶
from myelinate.client import GatewayClient
client = GatewayClient() # MYELINATE_URL (default http://localhost:8080), MYELINATE_API_KEY
# or explicitly:
client = GatewayClient("https://learner.internal", api_key="myl_...", timeout=10.0, retries=2)
Talks to a shared gateway over HTTP. Use this in production: knowledge is shared across replicas, frameworks and teams, and governance, review and audit happen centrally. Retries transport errors, 5xx and 429.
EmbeddedClient¶
from myelinate.client import EmbeddedClient
client = await EmbeddedClient(tenant_id="local").start() # creates tables, starts a background worker
...
await client.aclose()
Runs the learning engine in-process against MYELINATE_DATABASE_URL (default ./myelinate.db), with the same
settings as the gateway (MYELINATE_* env vars). Good for local development, single-node or edge deployments. There is
no auth and no REST API for reviewers, and the knowledge lives with that database.
Who is the user¶
The user id is read per run from session.state["myelinate_user_id"], falling back to the user_id passed to the
constructor. Set it once per session:
With a user id, recall includes that user's preferences and profile card, and remember calls from the agent are
scoped to that user. Without one, everything is agent-level.
Outcome and verification¶
outcome_fn and verify_fn receive MAF's SessionContext after the run; context.response is the agent response
and context.input_messages the input. Verification is the strongest learning signal: wire it to a real check if you
have one. See what to send.
from myelinate.models import Outcome, Verification
def outcome(ctx) -> Outcome:
text = ctx.response.text if ctx.response else ""
return Outcome.failure if "I can't help" in text else Outcome.unknown
def verify(ctx) -> Verification | None:
ticket = lookup_ticket(ctx) # your system of record
if ticket is None:
return None # no ground truth for this run
return Verification(passed=ticket.resolved, details=f"ticket {ticket.id}: {ticket.status}",
source="helpdesk")
learner = SelfLearningProvider(GatewayClient(), agent_id="support-bot", outcome_fn=outcome, verify_fn=verify)
Return Outcome.unknown when you don't know. With an LLM configured on the gateway, unknown outcomes are judged, and
only confident judgements can change skills.
Feedback after the run¶
Uploads are asynchronous. Call flush() first, then read the episode id from the session state:
from myelinate.models import FeedbackIn
await learner.flush()
episode_id = session.state["myelinate"]["last_episode_id"]
await client.feedback(FeedbackIn(episode_id=episode_id, score=0.0,
correction="Refunds for gift orders go to the purchaser."))
In a stateless service, look the episode up by session instead:
GET /v1/episodes/latest?agent_id=support-bot&session_id=<session.session_id>.
flush() and shutdown¶
Episodes upload in background tasks. Call await learner.flush() before the process exits (and in tests) so pending
uploads complete. flush() never raises; failed uploads are logged as myelinate.record_failed.
Combining with MAF telemetry¶
MAF emits OpenTelemetry GenAI spans. You can also send those to Myelinate for zero-code recording (see OpenTelemetry), but don't do both for the same agent: each run would be recorded twice under different idempotency keys. Use the provider when you want recall; it records too.
.NET¶
The adapter is Python only. For MAF on .NET:
- Call the REST API from an
AIContextProvider(or middleware):POST /v1/recallbefore the run,POST /v1/episodesafter. - Or connect the agent to the gateway's MCP endpoint as an MCP tool source and let the model call
recallandrecord_episode. - Or export MAF's OpenTelemetry traces to the gateway for recording (OpenTelemetry), plus a recall call.
Troubleshooting¶
| Symptom | Cause and fix |
|---|---|
ImportError: install the Microsoft Agent Framework |
Install the extra: pip install "myelinate[maf]". |
Warning myelinate.recall_failed |
Gateway unreachable, wrong MYELINATE_URL, or 401/403. The run continued without learned context. |
Warning myelinate.record_failed |
Same causes for the upload. Check the API key has the learn scope. |
| No episodes appear in short scripts | The process exited before the background upload finished. await learner.flush(). |
| Instructions are empty on some runs | Nothing relevant learned yet, or the run is in the 5% holdout group. Both are normal. |
| "The provided chat client does not support function invoking" | The chat client has no function-calling layer, so skill_view/remember can't be used. Use a client with function calling, or add_tools=False. |