22 min read

What I learned at Devoxx Morocco 2026: Day 2 🇲🇦

Key ideas from day two of Devoxx Morocco 2026, talk by talk, with interactive examples.

conferencedevoxxaiagents

The key ideas from the talks I attended on day two of Devoxx Morocco 2026, one section per talk.

Mete Atamel on stage, presenting a slide to a full room
Mete's talk
Attendees at the exhibitor stands between talks
Between talks
A colorful painting of three figures at the venue
More venue art
A laptop covered in conference stickers next to a Google tote bag
Swag haul
Me at the Devoxx Morocco photo booth
Photo booth
Talk 1
Tour of Agent Protocols: MCP, A2A, AG-UI, A2UI, AP2, UCP
Mete Atamel

Agents now have a protocol for almost every connection they make. They're easy to mix up, so here's the short version: each protocol covers one connection.

Six protocols, one agent
Pick a protocol to see what it connects
Your frontend
Your agent
MCP · Agent tools and data
modelcontextprotocol.io
What it is
Wraps functions, read-only data and ready-made prompts in a server that any AI app can use.
Use it when
Your agent needs to call an API, read files or query a database.
Started by
Anthropic
MCP and A2A are the ones most apps need today. AG-UI and A2UI cover the frontend, and UCP and AP2 are newer protocols for agents that shop and pay.
ProtocolConnectsUse it for
MCPAgent tools and dataGiving an agent functions, files and prompts
A2AAgent other agentsDelegating work to an agent someone else runs
AG-UIAgent your frontendStreaming messages, tool calls and state
A2UIAgent generated UILetting the agent answer with UI instead of text
UCPAgent online storesBrowsing a catalog and checking out
AP2Agent paymentsPaying within limits the user signed off on

The main advice: these are low-level protocols. As an application developer, you'll mostly use them through an agent framework (the talk used , but most frameworks support MCP and A2A) instead of implementing them yourself.

MCP: tools and data

MCP standardizes how an AI app reaches tools and data. You wrap your functions in an MCP server, and any AI app (the host) connects to it through one MCP client per server. Write the integration once, and every MCP-compatible app can use it.

A server exposes three kinds of things, and the difference is who decides to use them:

What it isWho triggers it
ToolsFunctions with input and outputThe model
ResourcesRead-only data, like documents or settingsThe app
PromptsReady-made prompt templates for using the serverThe user
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { z } from "zod";

const server = new McpServer({ name: "documents", version: "1.0.0" });

// Tool: the model decides to call it
server.registerTool(
	"edit_doc",
	{ description: "Replace a document's content", inputSchema: { id: z.string(), text: z.string() } },
	async ({ id, text }) => {
		await docs.save(id, text);
		return { content: [{ type: "text", text: `Saved ${id}` }] };
	}
);

// Resource: read-only data the app can load
server.registerResource("docs", "docs://list", { description: "All documents" }, async (uri) => ({
	contents: [{ uri: uri.href, text: JSON.stringify(await docs.list()) }],
}));

// Prompt: a template the user picks, which tells the model how to use the tools
server.registerPrompt(
	"format_doc",
	{ description: "Turn a plain-text document into Markdown", argsSchema: { id: z.string() } },
	({ id }) => ({
		messages: [{
			role: "user",
			content: { type: "text", text: `Reformat document ${id} as Markdown without changing its meaning, then save it with edit_doc.` },
		}],
	})
);
  • Transports: for a local server (handy for testing), Streamable HTTP for local or remote servers. Messages are , so they're readable on the wire.
  • The protocol is moving away from sessions. Clients and servers exchange their capabilities on each request instead of keeping state, which makes remote servers much easier to scale.
  • Two features worth knowing if you build servers: elicitation (a tool pauses to ask the user for missing information, like an approval) and progress notifications for long-running tools.
  • Finding servers: most of the time you'll use other people's servers. Look in the official MCP Registry or the GitHub MCP Registry.
  • Testing: MCP Inspector connects to any server and lets you call its tools, resources and prompts.

A2A: agents talking to agents

A2A standardizes how agents running on different frameworks and clouds talk to each other, like an ADK agent on Google Cloud asking an agent built with another framework on Azure. Agents running in the same app should just call each other directly. A2A is for remote ones.

Discovery happens through an agent card, a JSON file the agent serves at /.well-known/agent-card.json:

{
	"name": "Currency agent",
	"description": "Converts between currencies using live exchange rates",
	"url": "https://currency-agent.example.com",
	"version": "1.0.0",
	"capabilities": { "streaming": true, "pushNotifications": false },
	"defaultInputModes": ["text"],
	"defaultOutputModes": ["text"],
	"skills": [
		{
			"id": "get_exchange_rate",
			"name": "Exchange rate",
			"description": "Gets the exchange rate between two currencies",
			"examples": ["What is 1 GBP in USD?"]
		}
	]
}

Skills are A2A's name for what MCP calls tools. The card also lists capabilities, like streaming or push notifications, and how to authenticate.

  • Message or task: a simple question gets a plain message back. Longer work becomes a task with a status (working, completed, failed, canceled), and its results come back as artifacts.
  • Three ways to get results: wait for the response, stream it as , or receive push notifications if the client isn't always connected.
  • Let the framework do it. By hand, you write an agent executor (with execute and cancel methods) and an agent card. ADK generates both from a regular agent, and on the calling side it wraps a remote agent so it looks like a local one.
  • Testing: A2A Inspector fetches an agent's card and lets you chat with it. There's also an A2A CLI, which coding agents can use too.

AG-UI: agent to frontend

AG-UI, started by CopilotKit, standardizes the stream of events between an agent backend and its frontend, so any frontend can work with any agent framework:

{ "type": "RUN_STARTED", "threadId": "t1", "runId": "r1" }
{ "type": "TEXT_MESSAGE_START", "messageId": "m1", "role": "assistant" }
{ "type": "TEXT_MESSAGE_CONTENT", "messageId": "m1", "delta": "Checking flights" }
{ "type": "TOOL_CALL_START", "toolCallId": "c1", "toolCallName": "search_flights" }
{ "type": "STATE_DELTA", "delta": [{ "op": "replace", "path": "/status", "value": "searching" }] }
{ "type": "RUN_FINISHED", "threadId": "t1", "runId": "r1" }

The AG-UI Dojo shows working integrations for each framework, frontend and backend code side by side.

A2UI: the agent generates the UI

Agents are good at generating text, images and code, so why not UI? With A2UI, from Google, the agent outputs JSON that describes the UI, using only components from a known catalog (layouts, text, buttons, cards, tabs, and so on). Renderers for web and Flutter turn that JSON into native UI.

It's data, not code: the agent can only describe components the app already knows how to draw, so there's no generated code to trust. A few messages drive it: create a surface, update its components, update its data, and delete the surface when it's done.

  • AG-UI vs A2UI: AG-UI is the transport (how events and state reach the frontend). A2UI is the spec for what UI to draw. They work together.
  • It's not the only option for generative UI. MCP-UI and Open-JSON-UI exist too, and it's still early.
  • It's getting faster. Generating and rendering the JSON takes time, and a more compact format was recently announced.
  • Tools: the A2UI Composer shows every component and its JSON. Try it at a2ui.org.

AP2 and UCP: agents that shop and pay

Two newer protocols, for agents that buy things on your behalf.

AP2 (Agent Payments Protocol) makes agent payments signed and verifiable. The user signs that say what the agent may do:

  • Human present: a closed mandate. Buy exactly this item at this price.
  • Human not present: an open mandate with limits, like "under 100 euros, at most two". It's checked when the agent actually buys.
  • Signed receipts afterwards, as proof for audits.

Only the shopping agent is "agentic". The merchant and payment endpoints stay deterministic.

UCP (Universal Commerce Protocol, from Google and retail partners) covers the shopping itself. The agent discovers a merchant's profile, opens a checkout session, negotiates what both sides support, then goes through checkout states (incomplete when information is missing, then fulfillment and payment). It's designed to work with AP2 for the payment step.

Resources

Key takeaways
  • One connection, one protocol: MCP for tools and data, A2A for other agents, AG-UI and A2UI for the frontend, UCP and AP2 for shopping and paying.
  • Use a framework. It generates agent cards, executors and clients, so you rarely touch the protocols directly.
  • MCP has three primitives, split by who triggers them: tools (the model), resources (the app), prompts (the user).
  • Use A2A only for remote agents. Agents in the same app can call each other directly.
  • AG-UI moves events, A2UI describes UI. One is the transport, the other is the content.
  • Test with the inspectors (MCP Inspector, A2A Inspector) before wiring a server into an app.
Talk 2
My Fixes Became the Lesson: Teaching AI Better Coding Habits
François Martin

The core idea: when you keep correcting a coding agent on the same thing, turn those corrections into a skill, then prove with evals that the skill actually helps. The examples are Java Optional and Streams code, but the method works for any habit you want an agent to learn.

What a skill is

An is an open format that 40+ tools and can read. It's a folder with a SKILL.md file, plus optional extra files the agent opens when it needs them:

---
name: java-optionals
description: Write, review and refactor Java code that uses Optional. Use whenever writing, reviewing or refactoring Java code that reasons about Optional.
---

# Java Optional

- Prefer map / flatMap / orElse over isPresent() followed by get().
- …

For before / after examples, read references/examples.md.
  • The description is always loaded. For every installed skill, its description sits in the agent's context from the start, so each skill costs tokens. Don't install hundreds.
  • The description decides when the skill triggers. Write when to use it ("use whenever…"), not just what it is.
  • You can also call it by name: /java-optionals in Claude Code, $java-optionals in Codex.

Progressive disclosure

Don't put everything in one file. Keep SKILL.md to the essentials and point to other files for details and examples, so the agent only reads them when it needs them. The same applies to files.

Progressive disclosure
Tokens a Java Optional skill adds, step by step

The agent opens the skill. With one big file, everything comes in at once.

One big file8,457
Split skill117
java-optionals/description: always loaded
  • SKILL.md
  • references/rules.md
  • references/examples.md
A skill only pays for what the agent actually reads. Keep SKILL.md to the essentials and move examples and details into files it opens when needed.

Build the skill from your corrections

Once the agent finally produces what you wanted, ask it to write the skill from that same conversation:

Check the transcript of this conversation for what was bad and what was good.
Use that to create a skill for Optional code in Java.
Don't add things speculatively. Base it on what you implemented badly without my guidance.

Agents learn well from before and after examples, so the skill should show the bad attempt next to the better final version.

Then check that it works: check out the commit from before your first prompt, install the skill, and send the exact same first prompt. If the agent now gets it right in one round, the skill works. If not, improve the skill and replay again.

Turn the replay into evals

Replaying works, but you have to remember the commit and the prompt forever, and colleagues without access to your code can't run it. The fix is an , built like a good bug report: a minimal reproducible prompt.

  • One prompt that reliably triggers the problem, written the way someone would actually ask.
  • No hints at the answer. If the prompt mentions the right method, the eval proves nothing.
  • A checklist out of 100 points. For example: 60 for the right approach, 20 for avoiding anti-patterns, 20 for behavior and readability.

Each eval runs twice, with and without the skill, and an LLM grades both against the checklist. The agent never sees the checklist, and the grader never sees the skill. Tessl runs exactly this and publishes the results for each skill in its registry. With a good prompt and checklist, the scores turn out to be very consistent from run to run.

The number to watch is lift: points with the skill divided by points without it.

Is the skill worth its context?
Lift = points with the skill ÷ points without it
2.26xClearly worth it. Past 2x, more lift matters less
0x1x1.5x2x3x
The same eval prompts run once with the skill and once without, graded on the same checklist. Thresholds are the ones from the talk.

Evals also give the agent a goal it can work toward on its own: "Improve the skill until it scores 100% on all evals, without making it so specific that it copies the evals."

Read the evals: agents cheat

Agents writing their own evals will take shortcuts, like an eval that awards points for copying a command straight out of the skill. An eval that only the skill can pass is not a test. Before trusting a lift score, check:

  • Is the prompt realistic? Would anyone actually type it?
  • Could an agent without the skill pass at all? If it needs information that only the skill has, the comparison is unfair.
  • Are the points weighted toward what matters? If formatting is worth 90 points and correctness 10, the lift means nothing.
  • Does the skill contain the eval's code word for word? Similar shapes are fine, identical examples are not.

When the skill doesn't trigger, fix the description

If the agent writes bad code but fixes it as soon as you say "check with the Optional skill", the skill's content is fine. It just didn't trigger, so the description needs work, not the instructions.

Keep the skill learning

Corrections still happen after a skill ships, so close the loop:

  1. When he corrects the agent on code the skill covers, a skill files an issue on the skill's repository, with the code before, the corrected code, and why it changed.
  2. Until that issue is fixed in the skill, a file linked from AGENTS.md holds the new rule as a temporary override, so the agent already follows it.
  3. When the issue closes, the override is removed.

It made a visible difference. Across 1,800+ prompts in one project, 20 corrected Optional or Streams code: 17 before the skill was installed, 3 while it was being refined, and none in the two weeks after.

When to write a skill

  • You've corrected the agent twice for the same thing.
  • You're copying the same paragraph between AGENTS.md files in different repositories.
  • You keep reusing an old conversation because "it already knows what I want". Colleagues don't have that conversation, but they can install a skill.

A skill that builds skills

To skip the manual steps, there's build-a-skill. Run /build-a-skill in the conversation where the agent finally got it right. It finds where the problem started, writes the skill from your corrections only, turns your own cases into evals, then replays your first prompt and runs the evals with and without the skill. You still read the evals.

Resources

Key takeaways
  • Corrected the agent twice? Write a skill, built from that conversation, with before and after examples.
  • Keep SKILL.md small and move details into files the agent opens when needed.
  • Write the description for when to trigger. If the skill works when called by name but not on its own, fix the description.
  • Prove it with evals: a realistic prompt with no hints, a 100-point checklist, runs with and without the skill.
  • Look for a lift of 1.5x or more before letting a skill take up context.
  • Read the evals yourself. Agents will write evals that only the skill can pass.
  • Feed new corrections back into the skill, with a temporary override until it's updated.
Talk 3
Stop Building Technical Debt: Architecture Patterns for Startups That Scale
Natalie Godec

The core idea: you will migrate eventually, to a bigger setup, another region or another cloud. A few cheap decisions made early decide whether that migration takes a week or a year. None of them needs Kubernetes or Kafka.

It's fine to start simple. Here's what to do from day one anyway:

From "it works" to "it migrates"
The same small product, one decision at a time

One VM does everything. Fine for now, hard to move later.

  • ComputeVM + LambdaTied to Lambda's libraries
  • StorageS3
  • MessagingNoneDirect calls
  • DatabasePostgres on the VMWon't scale
  • MonitoringCloudWatch
3 migration risks
None of these steps needs Kubernetes or Kafka. They are cheap early on and expensive to retrofit later.

Containerize everything

Even the small script that downloads files and processes them. It takes minutes, especially with AI, and containers run anywhere: on a single VM today, on a serverless platform or Kubernetes later. You don't need Kubernetes to use containers.

Avoid code that only runs on one platform's function runtime. Code written against Lambda's own libraries is easy to start with and hard to move. Lambda can run container images too.

Postgres for relational data, a bucket for everything else

  • Relational data goes in Postgres. It's open source, supported everywhere, and every managed cloud database speaks it.
  • Everything else goes in object storage, caches included, instead of piling up MariaDB, Redis and friends you'll have to migrate one by one.
  • Write bucket data as . When someone wants to do data science on it later, it's already a data lake that BigQuery or Snowflake can query.

Go event-driven early, without Kafka

Event-driven design is harder than picking a database, which is why teams put it off. But retrofitting events into services that already call each other directly is far harder. Start with the simplest version:

  • Let the bucket trigger the work. Every cloud bucket can notify a queue when a file lands. The fetcher uploads, the queue gets a message, the processor picks it up. No polling, no orchestrator.
  • Use a serverless queue, not Kafka. Most teams use Kafka as a plain message queue, so SQS/SNS, Pub/Sub or Azure Service Bus do the job without brokers to manage.
  • Use an so database changes become events too:
await db.transaction(async (tx) => {
	await tx.insert(orders).values(order);
	// Same transaction: the event exists if and only if the order does
	await tx.insert(outbox).values({ type: "OrderPlaced", payload: order });
});
// A small relay publishes outbox rows to the queue and marks them as sent

Security you'll forget

  • No static keys. Use short-lived tokens and workload identity.
  • Give roles to groups, not users, even when the team is three people.
  • No on databases and internal services. Put a load balancer or private connection in front instead.
  • Keep secrets in a secret manager and mount them as environment variables. Every cloud supports this.
  • Harden every pod by default, and sandbox anything that runs agent-written code with or Kata Containers, since a normal container can be escaped:
apiVersion: v1
kind: Pod
metadata:
  name: agent
spec:
  automountServiceAccountToken: false
  runtimeClassName: gvisor # sandboxed runtime for agents
  securityContext:
    runAsNonRoot: true
    seccompProfile:
      type: RuntimeDefault
  containers:
    - name: agent
      image: europe-docker.pkg.dev/acme/agents/agent:1.4.0
      securityContext:
        allowPrivilegeEscalation: false
        readOnlyRootFilesystem: true
        capabilities:
          drop: ["ALL"]

Name things consistently

Pick one naming convention and use it for every resource. It looks like a detail, but inconsistent names mean you can't write one Terraform module for every environment and region.

One naming convention, everywhere
prefix · resource · region · environment
Resource
devoxx-ma-gke-europe-west1-dev
devoxx-ma-gke-europe-west1-uat
devoxx-ma-gke-europe-west1-prod
devoxx-ma-gke-me-central2-dev
devoxx-ma-gke-me-central2-uat
devoxx-ma-gke-me-central2-prod

■ region · ■ environment

Keep region and environment at the end. They are what changes, so new environments and regions become a loop in Terraform instead of a new naming puzzle.

Don't overdo it either. A convention that forces a two-letter country code breaks the first time a project spans a whole region, or is global. Make names readable and descriptive within your constraints.

Give your AI agents your standards

When everyone generates infrastructure with AI, every agent reinvents its own patterns and rereads the whole repo to add one resource. Write your engineering standards down where agents can find them:

  • Point an agent at your existing repos to extract the patterns you actually use, then edit them into rule files: naming, network layout, default cluster settings, how databases connect privately.
  • Share them across projects: a central AGENTS.md that points to the rule files, plus an MCP server that serves your standard Terraform modules and pipelines, installable with one command.
  • Verify the agent read them. Agents are non-deterministic and sometimes skip the rules entirely. Asking "did you check our standards?" often makes them rewrite the code to match.

Two migrations, two lessons

"It's just a container" can be a monolith in disguise. A small Azure app (one Windows VM, SQL Server, one container) looked like a one-week move to Cloud Run. The "container" turned out to serve the frontend, run background jobs, orchestrate and fetch third-party data, so it could neither scale to zero nor scale out. It still moved, but only by making Cloud Run behave like an always-on server: a minimum of one instance, no CPU throttling, and session affinity. Ask what's really inside before planning a migration.

A complex platform built on standard parts moves easily. 160 microservices in four regions on EKS, RDS Postgres, managed Kafka and S3 moved to GKE, AlloyDB, Kafka on Kubernetes (Strimzi) and Cloud Storage. Kafka was mirrored and Postgres replicated both ways with pglogical, so for a while it ran on both clouds at once. Containers, Postgres and standard messaging made that possible.

And one more: don't store non-relational data in a relational database. Email bodies kept in SQL Server made the database expensive to run and very hard to migrate. They belong in a bucket.

Resources

Key takeaways
  • Containerize everything, even small scripts. Kubernetes is optional, containers are not.
  • Postgres for relational data, a bucket for the rest, written as Parquet or Iceberg.
  • Go event-driven early with bucket notifications, a serverless queue and an outbox. Skip Kafka until you truly need it.
  • No static keys, no public IPs, roles on groups, secrets in a secret manager, hardened pods, and sandboxes for agents.
  • One readable naming convention, with region and environment at the end.
  • Write your standards down for your AI agents, and check that they read them.
  • Know what's really in "just a container" before you plan a migration.
Talk 4
Own Your Agent
Henry, Mistral AI

The core idea: every prompt you send to a hosted API runs on a model you can't inspect, on servers you'll never see. For anything that touches sensitive data, own your agent: run an inside your own infrastructure.

Why it's realistic now

  • An agent is a model plus a harness. The loop, the tools, the prompts and the guardrails matter as much as the model. The same model can score very differently depending on the harness, so pick the harness for your model, not because it's trendy.
  • Open-weight models are 3 to 6 months behind the frontier, down from about a year. Small models now match last year's large ones, so not every task needs the biggest model.
  • They fit on less hardware than you'd think. to 8 or 4 bits makes a model 2 to 4 times smaller for a quality loss of a few percent, and quantized versions are usually ready to download.

API or self-hosted?

Hosted APISelf-hosted
Best forFrontier performance, low volume, getting startedHigh volume, sensitive data
CostPer token, and every agent loop pays againFixed: one loop or twenty costs the same
The modelCan change or get worse without noticeExactly the version you deployed
Your dataLeaves your networkStays in your VPC or on-prem

At company scale, self-hosting breaks even quickly. The bigger reason is data: a useful agent needs your email, documents and tools, and the safest data transfer is the one that never happens. Hosted or not, still ask whether you need an agent at all. A classic ML pipeline does many jobs faster and cheaper.

The open-source stack

  • Model: open weights from Hugging Face.
  • Your data: Postgres with pgvector for search, LiveKit Agents for voice.
  • Prototype locally with llama.cpp or Ollama.
  • Serve in production with vLLM. It exposes an OpenAI-compatible API, so switching often means changing a base URL, and it can route simple tasks to smaller models.
  • Gateway and priorities: LiteLLM for API keys and usage tracking across teams, llm-d for scheduling and priorities across GPUs.
  • Observe everything with OpenTelemetry, sent to a backend like Langfuse: which tools the agent used, why, at what cost, and which deployed models nobody uses.
// Same client code: only the base URL changes from a hosted API to your own vLLM
const llm = new OpenAI({ baseURL: "http://vllm.internal:8000/v1", apiKey: process.env.LLM_KEY });

To migrate: find the agents that use the most tokens, move a non-sensitive one to an open-weight model first, evaluate it against the API version, and switch once the results match.

Key takeaways
  • For sensitive data, run an open-weight model in your own infrastructure.
  • Choose the harness as carefully as the model.
  • Self-host at volume: fixed cost, a stable model and no data leaving your network. Use APIs for frontier needs and low volume.
  • Quantize to fit smaller GPUs for a small quality cost.
  • Use vLLM's OpenAI-compatible API so switching is mostly a base URL, and trace everything with OpenTelemetry.
  • Not everything needs an agent.