<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/"><channel><title>Agent-Architecture on Damian Galarza | Software Engineering &amp; AI Consulting</title><link>https://www.damiangalarza.com/tags/agent-architecture/</link><description>Recent posts from Damian Galarza | Software Engineering &amp; AI Consulting</description><generator>Hugo</generator><language>en-us</language><managingEditor>Damian Galarza</managingEditor><atom:link href="https://www.damiangalarza.com/tags/agent-architecture/feed.xml" rel="self" type="application/rss+xml"/><item><title>Governing AI Agents Without Killing Them: What Actually Works in Production</title><link>https://www.damiangalarza.com/posts/2026-04-22-governing-ai-agents-without-killing-them/</link><pubDate>Wed, 22 Apr 2026 00:00:00 -0400</pubDate><author>Damian Galarza</author><guid>https://www.damiangalarza.com/posts/2026-04-22-governing-ai-agents-without-killing-them/</guid><description>Most AI agent governance advice targets boards, not builders. Three failure patterns, real TypeScript examples, and what a CTO should do Monday morning.</description><content:encoded><![CDATA[<p><a href="https://hoolahoop.io/articles/cto-coaching/agentic-ai-governance/">Agentic AI governance for CTOs</a> argues governance needs to come before deployment, not after. The strategic frame is right about what&rsquo;s at stake: organizational accountability, observability, and tool access. But the solutions assume organizational machinery most early-stage teams don&rsquo;t have. A two-person startup running a multi-agent system doesn&rsquo;t need a RACI. It needs a guardrail processor that fails loudly. Leigh names the tension directly: overly restrictive governance drives experimentation underground. Governance that lives in code resolves it — lightweight enough for a seed-stage team, enforceable enough for a regulator.</p>
<p>The piece covers six governance gaps. This post is about three where code-level enforcement most obviously beats policy — tool access, observability, and human-in-the-loop. Cost visibility, shadow AI, and accountability chains are real concerns that deserve their own treatment.</p>
<p>I&rsquo;ve spent the last several months building <a href="/posts/2026-03-06-build-personal-ai-assistant/">a multi-agent AI assistant</a> that runs my consulting business: CRM, email, calendar, invoicing, content pipeline, Slack across two workspaces. Before that, years building software in regulated healthcare, including work on 510(k)-cleared medical device software where every system decision needed an audit trail. &ldquo;We&rsquo;ll add logging later&rdquo; was never an acceptable answer when a regulator could ask to reconstruct any action the system took. That mindset shapes how I think about agent governance. The three patterns below are ones I&rsquo;ve either hit in production or narrowly avoided. Each is a place where code-level governance beats policy-level governance for a team that can&rsquo;t afford a review board.</p>
<h2 id="tool-sprawl-widens-your-blast-radius">Tool Sprawl Widens Your Blast Radius</h2>
<p>MCP server sprawl is named in the original frame as a source of expanded blast radius. The same governance principle lives one layer down, inside the agent&rsquo;s tool definition: every tool an agent can access is a tool it could misuse. An agent with access to email, calendar, invoicing, CRM, and file operations has a blast radius that spans your entire business. A single prompt injection or hallucination can reach tools the agent should never touch. The principle is least privilege at the agent level, not the system level. Each agent should have access to exactly the tools it needs for its role, and nothing else.</p>
<p>What makes this worse is that tool sprawl also degrades the agent&rsquo;s ability to do its job. An agent with 40 tools when it regularly uses 8 faces two compounding problems. Every tool definition consumes context window tokens, space the model can&rsquo;t use for reasoning about the actual task. And the model has to select the right tool from a larger set, which increases the odds of misselection. I&rsquo;ve watched agents pick a vaguely-similar tool over the correct one because the tool list was too long for the model to evaluate carefully. The governance risk and the performance cost come from the same root cause: too many tools in one agent&rsquo;s definition.</p>
<p>In my system, I run a multi-agent architecture where a supervisor delegates to domain-specific agents. I built the first version with a supervisor that had access to everything — why not let it figure out what to use? It worked in demos. In production, both problems showed up immediately: the supervisor&rsquo;s blast radius spanned the entire system, and the model wasted reasoning capacity navigating tools it didn&rsquo;t need.</p>
<p>Here&rsquo;s how I structure it instead. Each agent gets a scoped tool set:</p>
<div class="highlight"><pre tabindex="0" style="color:#cdd6f4;background-color:#1e1e2e;-moz-tab-size:2;-o-tab-size:2;tab-size:2;"><code class="language-typescript" data-lang="typescript"><span style="display:flex;"><span><span style="color:#6c7086;font-style:italic">// Each agent declares only the tools it needs
</span></span></span><span style="display:flex;"><span><span style="color:#6c7086;font-style:italic"></span><span style="color:#cba6f7">const</span> relayAgent <span style="color:#89dceb;font-weight:bold">=</span> <span style="color:#cba6f7">new</span> Agent({
</span></span><span style="display:flex;"><span>  name<span style="color:#89dceb;font-weight:bold">:</span> <span style="color:#a6e3a1">&#34;relay&#34;</span>,
</span></span><span style="display:flex;"><span>  instructions: <span style="color:#f38ba8">relayInstructions</span>,
</span></span><span style="display:flex;"><span>  model: <span style="color:#f38ba8">LOCAL_MODEL_LARGE_THINKING</span>,
</span></span><span style="display:flex;"><span>  tools<span style="color:#89dceb;font-weight:bold">:</span> {
</span></span><span style="display:flex;"><span>    <span style="color:#6c7086;font-style:italic">// Email tools only - no CRM, no calendar, no invoicing
</span></span></span><span style="display:flex;"><span><span style="color:#6c7086;font-style:italic"></span>    scanInbox,
</span></span><span style="display:flex;"><span>    readEmail,
</span></span><span style="display:flex;"><span>    readEmailThread,
</span></span><span style="display:flex;"><span>    labelEmail,
</span></span><span style="display:flex;"><span>    archiveEmail,
</span></span><span style="display:flex;"><span>    composeEmail,
</span></span><span style="display:flex;"><span>    draftEmail,
</span></span><span style="display:flex;"><span>    replyToEmail,
</span></span><span style="display:flex;"><span>  },
</span></span><span style="display:flex;"><span>});
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#cba6f7">const</span> tempoAgent <span style="color:#89dceb;font-weight:bold">=</span> <span style="color:#cba6f7">new</span> Agent({
</span></span><span style="display:flex;"><span>  name<span style="color:#89dceb;font-weight:bold">:</span> <span style="color:#a6e3a1">&#34;tempo&#34;</span>,
</span></span><span style="display:flex;"><span>  instructions: <span style="color:#f38ba8">tempoInstructions</span>,
</span></span><span style="display:flex;"><span>  model: <span style="color:#f38ba8">FAST_MODEL</span>,
</span></span><span style="display:flex;"><span>  tools<span style="color:#89dceb;font-weight:bold">:</span> {
</span></span><span style="display:flex;"><span>    <span style="color:#6c7086;font-style:italic">// Calendar tools only - no email, no CRM, no invoicing
</span></span></span><span style="display:flex;"><span><span style="color:#6c7086;font-style:italic"></span>    listCalendarEvents,
</span></span><span style="display:flex;"><span>    getCalendarEvent,
</span></span><span style="display:flex;"><span>    createCalendarEvent,
</span></span><span style="display:flex;"><span>    updateCalendarEvent,
</span></span><span style="display:flex;"><span>    deleteCalendarEvent,
</span></span><span style="display:flex;"><span>    findCalendarFreeBusy,
</span></span><span style="display:flex;"><span>  },
</span></span><span style="display:flex;"><span>});
</span></span></code></pre></div><p>The email agent can&rsquo;t touch the calendar. The calendar agent can&rsquo;t read emails. The invoicing agent can&rsquo;t send Slack messages to the shared workspace. These boundaries aren&rsquo;t documentation. They&rsquo;re structural. An agent literally cannot call a tool it doesn&rsquo;t have.</p>
<p>But tool scoping alone isn&rsquo;t enough. Some tools within an agent&rsquo;s set need additional constraints. My email agent has tools for composing and sending emails. The model can hallucinate plausible-looking recipient addresses, fabricate domains, or construct emails to addresses that don&rsquo;t exist. Instructions alone won&rsquo;t prevent this because the model can reason past them.</p>
<p>Rather than trusting the model&rsquo;s judgment, I enforce this at the framework level using <a href="https://mastra.ai/docs/agents/processors">Mastra&rsquo;s output processors</a>:</p>
<div class="highlight"><pre tabindex="0" style="color:#cdd6f4;background-color:#1e1e2e;-moz-tab-size:2;-o-tab-size:2;tab-size:2;"><code class="language-typescript" data-lang="typescript"><span style="display:flex;"><span><span style="color:#6c7086;font-style:italic">// A Mastra processor that blocks emails to fabricated addresses
</span></span></span><span style="display:flex;"><span><span style="color:#6c7086;font-style:italic"></span><span style="color:#cba6f7">import</span> <span style="color:#cba6f7">type</span> { ProcessOutputStepArgs, Processor } <span style="color:#cba6f7">from</span> <span style="color:#a6e3a1">&#34;@mastra/core/processors&#34;</span>;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#cba6f7">const</span> SEND_TOOLS <span style="color:#89dceb;font-weight:bold">=</span> <span style="color:#cba6f7">new</span> Set([<span style="color:#a6e3a1">&#34;compose-email&#34;</span>, <span style="color:#a6e3a1">&#34;reply-to-email&#34;</span>]);
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#cba6f7">export</span> <span style="color:#cba6f7">class</span> EmailSendGuardrailProcessor <span style="color:#cba6f7">implements</span> Processor<span style="color:#89dceb;font-weight:bold">&lt;</span><span style="color:#a6e3a1">&#34;email-send-guardrail&#34;</span><span style="color:#89dceb;font-weight:bold">&gt;</span> {
</span></span><span style="display:flex;"><span>  <span style="color:#cba6f7">readonly</span> id <span style="color:#89dceb;font-weight:bold">=</span> <span style="color:#a6e3a1">&#34;email-send-guardrail&#34;</span> <span style="color:#cba6f7">as</span> <span style="color:#cba6f7">const</span>;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>  processOutputStep({ toolCalls, abort, messages }<span style="color:#89dceb;font-weight:bold">:</span> ProcessOutputStepArgs) {
</span></span><span style="display:flex;"><span>    <span style="color:#cba6f7">if</span> (<span style="color:#89dceb;font-weight:bold">!</span>toolCalls<span style="color:#89dceb;font-weight:bold">?</span>.length) <span style="color:#cba6f7">return</span> messages;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#cba6f7">for</span> (<span style="color:#cba6f7">const</span> tc <span style="color:#cba6f7">of</span> toolCalls) {
</span></span><span style="display:flex;"><span>      <span style="color:#cba6f7">if</span> (<span style="color:#89dceb;font-weight:bold">!</span>SEND_TOOLS.has(tc.toolName)) <span style="color:#cba6f7">continue</span>;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>      <span style="color:#cba6f7">const</span> to <span style="color:#89dceb;font-weight:bold">=</span> (tc.args <span style="color:#cba6f7">as</span> { to?: <span style="color:#f38ba8">string</span> })<span style="color:#89dceb;font-weight:bold">?</span>.to;
</span></span><span style="display:flex;"><span>      <span style="color:#cba6f7">if</span> (<span style="color:#89dceb;font-weight:bold">!</span>to) <span style="color:#cba6f7">continue</span>;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>      <span style="color:#6c7086;font-style:italic">// Block obviously fabricated or placeholder recipients
</span></span></span><span style="display:flex;"><span><span style="color:#6c7086;font-style:italic"></span>      <span style="color:#cba6f7">if</span> (<span style="color:#94e2d5">/(@example\.com|@test\.com|@placeholder\.)/</span>.test(to) <span style="color:#89dceb;font-weight:bold">||</span> <span style="color:#89dceb;font-weight:bold">!</span>to.includes(<span style="color:#a6e3a1">&#34;@&#34;</span>)) {
</span></span><span style="display:flex;"><span>        abort(
</span></span><span style="display:flex;"><span>          <span style="color:#a6e3a1">`The recipient &#34;</span><span style="color:#a6e3a1">${</span>to<span style="color:#a6e3a1">}</span><span style="color:#a6e3a1">&#34; looks like a guessed address. Look up the contact in the CRM first. Never fabricate email addresses.`</span>,
</span></span><span style="display:flex;"><span>          { retry: <span style="color:#f38ba8">true</span> },
</span></span><span style="display:flex;"><span>        );
</span></span><span style="display:flex;"><span>        <span style="color:#cba6f7">return</span> messages;
</span></span><span style="display:flex;"><span>      }
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#cba6f7">return</span> messages;
</span></span><span style="display:flex;"><span>  }
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>Processors inspect the step&rsquo;s generated tool calls and can abort execution with a retry hint when something violates a hard rule. If the model hallucinates a recipient address, the guardrail aborts with a message telling the agent to look up the contact in the CRM first. The address never reaches the send tool. No approval card, no prompt-based workaround.</p>
<p>The same principle applies to trust boundaries across workspaces. I run two Slack integrations: one for my private workspace, one for a shared community. The community-facing agent has no browser access, no credential vault, no file system. That&rsquo;s not a policy document. It&rsquo;s a different agent with a different tool set, pointed at a different Slack app.</p>
<p><strong>The pattern:</strong> Don&rsquo;t govern tool access with policies that agents might ignore. Remove the tools from the agent&rsquo;s definition entirely. Governance you can&rsquo;t violate is better than governance you promise to follow.</p>
<h2 id="beyond-tracing-structured-decision-logs-for-agent-governance">Beyond Tracing: Structured Decision Logs for Agent Governance</h2>
<p>You cannot govern what you cannot see. Modern tracing tools like Phoenix Arize, Langfuse, and Mastra Studio show you the full request/response cycle: inputs, outputs, tool calls, latency, and the model&rsquo;s reasoning process. I use Phoenix Arize extensively. It&rsquo;s the first place I look when debugging why an agent picked the wrong tool, hallucinated a parameter, or took an unexpected path.</p>
<p>Tracing is essential, but it answers a specific class of questions: <em>what happened inside the model&rsquo;s reasoning</em>. Governance needs a second layer: structured decision logs that answer <em>what the system decided, what confidence it had, and whether the outcome was correct in your domain context</em>.</p>
<p>This is familiar territory if you&rsquo;ve worked in regulated environments. In healthcare software, particularly anything touching the 510(k) pathway for Software as a Medical Device (SaMD), you don&rsquo;t just log that a record was modified. You log who modified it, when, what the previous value was, and what rule authorized the change. Every action must be reconstructable because a regulator will ask. Agent governance has the same shape, even outside healthcare. The stakeholder asking &ldquo;why did the agent do that?&rdquo; isn&rsquo;t debugging model behavior. They&rsquo;re asking whether the outcome was correct given the business rules, and they need a trail that answers that question without ambiguity.</p>
<p>Here&rsquo;s the distinction in practice. When my email triage agent archives a message, I can see in Phoenix Arize exactly what the model received and how it reasoned about the classification. That&rsquo;s useful for debugging why the model chose &ldquo;archive&rdquo; over &ldquo;escalate.&rdquo; But when I need to answer &ldquo;show me every email that was auto-archived from my inbox last week, what confidence level each had, and which ruleset applied,&rdquo; I need structured logs that are queryable independent of the tracing system.</p>
<p>That means capturing the agent&rsquo;s decision context in a structured, queryable format:</p>
<div class="highlight"><pre tabindex="0" style="color:#cdd6f4;background-color:#1e1e2e;-moz-tab-size:2;-o-tab-size:2;tab-size:2;"><code class="language-typescript" data-lang="typescript"><span style="display:flex;"><span><span style="color:#6c7086;font-style:italic">// Each triage decision captures full context for audit
</span></span></span><span style="display:flex;"><span><span style="color:#6c7086;font-style:italic"></span><span style="color:#cba6f7">interface</span> TriageDecision {
</span></span><span style="display:flex;"><span>  messageId: <span style="color:#f38ba8">string</span>;
</span></span><span style="display:flex;"><span>  subject: <span style="color:#f38ba8">string</span>;
</span></span><span style="display:flex;"><span>  <span style="color:#cba6f7">from</span><span style="color:#89dceb;font-weight:bold">:</span> <span style="color:#f38ba8">string</span>;
</span></span><span style="display:flex;"><span>  classification<span style="color:#89dceb;font-weight:bold">:</span> <span style="color:#a6e3a1">&#34;archive&#34;</span> <span style="color:#89dceb;font-weight:bold">|</span> <span style="color:#a6e3a1">&#34;act&#34;</span> <span style="color:#89dceb;font-weight:bold">|</span> <span style="color:#a6e3a1">&#34;digest&#34;</span> <span style="color:#89dceb;font-weight:bold">|</span> <span style="color:#a6e3a1">&#34;escalate&#34;</span>;
</span></span><span style="display:flex;"><span>  confidence: <span style="color:#f38ba8">number</span>;
</span></span><span style="display:flex;"><span>  mode<span style="color:#89dceb;font-weight:bold">:</span> <span style="color:#a6e3a1">&#34;conservative&#34;</span> <span style="color:#89dceb;font-weight:bold">|</span> <span style="color:#a6e3a1">&#34;full&#34;</span>;   <span style="color:#6c7086;font-style:italic">// Which ruleset applied
</span></span></span><span style="display:flex;"><span><span style="color:#6c7086;font-style:italic"></span>  reason: <span style="color:#f38ba8">string</span>;                  <span style="color:#6c7086;font-style:italic">// Why the agent chose this
</span></span></span><span style="display:flex;"><span><span style="color:#6c7086;font-style:italic"></span>  actionTaken: <span style="color:#f38ba8">string</span>;             <span style="color:#6c7086;font-style:italic">// What actually happened
</span></span></span><span style="display:flex;"><span><span style="color:#6c7086;font-style:italic"></span>  labels: <span style="color:#f38ba8">string</span>[];                <span style="color:#6c7086;font-style:italic">// What labels were applied
</span></span></span><span style="display:flex;"><span><span style="color:#6c7086;font-style:italic"></span>  timestamp: <span style="color:#f38ba8">string</span>;
</span></span><span style="display:flex;"><span>}
</span></span><span style="display:flex;"><span><span style="color:#6c7086;font-style:italic">// Persisted to the database, queryable from the dashboard
</span></span></span></code></pre></div><p>The triage system runs two different modes depending on whose inbox it&rsquo;s scanning. My inbox gets conservative mode (only auto-archives high-confidence machine-generated noise like billing receipts and marketing emails). The AI assistant&rsquo;s inbox gets full mode (four classification categories, auto-archives everything after classification). That modal distinction matters for governance because the blast radius is different. Archiving a marketing email from the assistant&rsquo;s inbox is low stakes. Archiving something from my inbox that I hadn&rsquo;t seen yet is a different conversation.</p>
<p>Beyond decision logging, I track errors with fingerprint deduplication. Every catch block writes to a structured error table with module, message, and context. A background health monitor runs every five minutes, detects stale processes, and escalates to the LLM for analysis when rule-based detection isn&rsquo;t enough. The dashboard surfaces all of this: health banners, error pages with filters, and stale-session badges that go amber after 10 minutes and red after 30.</p>
<p>None of this came from a governance framework. It came from an earlier system I built called Tracewell AI, where agents generated design inputs from source material in a regulated context. Every derivation had to be auditable: &ldquo;show me every design input, which sources the agent pulled from, and its confidence at the time.&rdquo; No trace format could answer that, and I wasn&rsquo;t using one. I built a structured audit log because compliance required it, not because debugging demanded it. That&rsquo;s where I learned the distinction: traces show what the model reasoned; structured logs show what the system decided, under which rules, with what confidence.</p>
<p><strong>The pattern:</strong> Tracing gives you deep visibility into model behavior. Use it. But for governance, pair it with structured decision logs that capture domain-specific context: what was decided, what confidence level, what ruleset applied, and what action the system took. Make both queryable, and make sure someone is actually reviewing them.</p>
<h2 id="human-in-the-loop-the-checkpoint-that-actually-works">Human-in-the-Loop: The Checkpoint That Actually Works</h2>
<p>The most important insight about human-in-the-loop is the &ldquo;rubber-stamp trap.&rdquo; Adding human review to every agent decision is a common starting point. In practice, reviewers get overwhelmed, start rubber-stamping, and the checkpoint becomes theater.</p>
<p>This isn&rsquo;t just a theory. Anthropic recently published <a href="https://www.anthropic.com/engineering/claude-code-auto-mode">research on Claude Code&rsquo;s auto-accept mode</a> that quantifies the problem: users were approving 93% of permission prompts. That&rsquo;s not review. That&rsquo;s muscle memory. Their solution was to replace blanket approval with a tiered system where a model-based classifier evaluates risk and only escalates actions that warrant human attention. The classifier uses a two-stage pipeline (fast filter, then chain-of-thought reasoning) and catches overeager behavior, prompt injection, scope escalation, and honest mistakes while letting routine actions through without friction.</p>
<p>The same principle applies to agent systems. The solution isn&rsquo;t removing human review. It&rsquo;s being precise about <em>where</em> it adds value and <em>what context</em> the reviewer needs to make a real decision.</p>
<p>My system uses a tiered approach. Low-risk actions (reading emails, looking up calendar events, searching the CRM) happen without approval. The agent just does them. High-risk actions go through explicit approval gates using <a href="https://mastra.ai/docs/agents/agent-approval">Mastra&rsquo;s agent approval system</a>. When a tool is tagged with <code>requireApproval: true</code>, Mastra pauses execution at the framework level before the tool runs. The stream emits an approval event with the tool name and arguments, and the tool only executes after an explicit <code>approveToolCall()</code>. This is framework-enforced, not prompt-based, so the model can&rsquo;t reason its way past the gate.</p>
<p>The key design choice is what &ldquo;approval&rdquo; looks like. A generic &ldquo;Agent wants to perform an action. Approve?&rdquo; dialog is useless. The reviewer has no context, so they either rubber-stamp or block everything out of caution. Both outcomes are governance failures.</p>
<p>Here&rsquo;s what a real approval checkpoint looks like for my coding pipeline:</p>
<pre tabindex="0"><code>planning → risk assessment → low risk? ──yes──→ auto-approved → executing
                                ↓ no
                          plan_review → approved → executing
                                ↑              ↓
                            revise ← request changes
</code></pre><p>The agent generates a plan. A risk assessor (two layers: deterministic heuristics for hard stops like <code>DROP TABLE</code> or <code>.env</code> modifications, plus an LLM classifier for everything else) evaluates the plan. Low-risk plans auto-approve and execute immediately. Medium and high-risk plans go to human review with the full plan visible, not just a yes/no prompt.</p>
<p>When I review a plan, I see exactly what the agent intends to do, which files it will touch, and why the risk assessor flagged it. I can approve, request changes (the agent revises and resubmits), or reject entirely. That&rsquo;s a checkpoint with teeth. The reviewer has enough context to make a real judgment call, and the &ldquo;request changes&rdquo; path means the review isn&rsquo;t binary.</p>
<p>For email, the approval is even more specific. When the agent wants to send an email, the approval card shows the full email: recipient, subject, body. I&rsquo;m not approving &ldquo;send an email.&rdquo; I&rsquo;m approving <em>this specific email to this specific person</em>. The context makes the checkpoint real instead of performative.</p>
<p>The less obvious lesson: the approval system itself can break in ways that look like it&rsquo;s working. I discovered that tagging certain tools with <code>requireApproval</code> caused my supervisor agent to avoid delegating to the sub-agent entirely. The supervisor model saw that the delegation path was &ldquo;approval-gated&rdquo; and hallucinated reasons not to use it. The approval mechanism was technically present but functionally disabled because the model routed around it. I only caught this by checking the traces (see: observability matters).</p>
<p><strong>The pattern:</strong> Approval checkpoints work when three conditions are met. The reviewer sees the full context of the action, not just a generic prompt. Low-risk actions bypass review entirely so the reviewer isn&rsquo;t fatigued. And the system is monitored to ensure the approval path is actually being exercised, not silently avoided.</p>
<h2 id="governance-as-code-defense-in-depth">Governance as Code: Defense in Depth</h2>
<p>Each of the previous patterns (tool scoping, guardrail processors, decision logs, approval gates) is a single layer. None of them is sufficient alone. The real value shows up when you stack them.</p>
<p>Take sending email as the running example. You&rsquo;ve already seen the individual layers: the email guardrail processor that blocks fabricated recipients, and the approval gate that pauses execution for human review. Here&rsquo;s how they combine with two additional layers into a defense-in-depth stack:</p>
<ol>
<li><strong>Tool API design</strong> forces an explicit <code>sender</code> parameter (&ldquo;emma&rdquo; | &ldquo;damian&rdquo;) with no default. The caller must deliberately choose which account sends.</li>
<li><strong>Guardrail processor</strong> blocks fabricated or placeholder recipients before the tool executes. Hard abort, no workaround.</li>
<li><strong>Framework-level approval gate</strong> (<code>requireApproval: true</code>) pauses execution and surfaces the full email for review: recipient, subject, body, sender.</li>
<li><strong>Client-level enforcement</strong> in <code>sendEmail()</code> requires an explicit <code>userEmail</code> argument. No fallback, no default. If the parameter is missing, it throws.</li>
</ol>
<p>Each layer is independent. If the model hallucinates a recipient, Layer 2 catches it. If it tries to send without approval, Layer 3 blocks it. If somehow the tool args are malformed, Layer 4 throws. A bypass at one layer doesn&rsquo;t compromise the others.</p>
<p>That&rsquo;s what governance as code means. The constraints are enforced by the system, verified by tests, and visible in the codebase, not buried in a Confluence page. This is one of the <a href="/posts/2026-03-25-four-patterns-that-separate-agent-ready-codebases/">dimensions that separate agent-ready codebases</a> from ones that break under real workloads. Frameworks like <a href="https://mastra.ai/docs/agents/guardrails">Mastra</a> give you the primitives: guardrail processors for hard rules, approval gates for human review. Your job is to wire them into a layered defense that matches your risk profile.</p>
<h2 id="what-id-tell-a-cto-to-do-monday-morning">What I&rsquo;d Tell a CTO to Do Monday Morning</h2>
<p>If you&rsquo;re leading a team that&rsquo;s deploying agents, here&rsquo;s where to start:</p>
<p><strong>Audit your tool surfaces.</strong> For every agent in production, list the tools it has access to and ask: does this agent need all of these? Every unnecessary tool is expanded blast radius and wasted context window. Scope them down. You&rsquo;ll likely see better tool selection as a side effect.</p>
<p><strong>Add structured decision logging to your highest-stakes agent action.</strong> You probably already have LLM tracing. Pick the one action where you&rsquo;d need to explain &ldquo;why did the agent do that?&rdquo; to a stakeholder, and add structured logs that capture the decision context: inputs, classification, confidence, action taken. Make it queryable from your dashboard, not buried in trace spans.</p>
<p><strong>Pick your highest-risk action and build a real checkpoint.</strong> Not a generic approval dialog. An approval flow that shows the reviewer the full context of the action. Frameworks like <a href="https://mastra.ai/docs/agents/agent-approval">Mastra</a> provide the primitives. One real checkpoint is worth more than twenty rubber-stamp prompts.</p>
<p><strong>Move one governance rule from documentation to code.</strong> Find a constraint that&rsquo;s currently a line in a README or a team agreement. Encode it as a guardrail processor, a test, a structural boundary. Something that fails loudly when violated rather than depending on an agent reading and following instructions.</p>
<p>These are afternoon-sized tasks, not quarterly initiatives. That&rsquo;s the point. The original frame is right that governance has to come before agents ship at scale. But &ldquo;before&rdquo; doesn&rsquo;t mean organizational review boards. It means constraints in code that ship with the agent. Each of these gives you something concrete: a tighter tool surface, a queryable decision log, an approval checkpoint that someone actually uses, or a constraint that enforces itself without depending on an agent&rsquo;s good behavior.</p>
<p>If you want a faster read on where you stand, I built a companion <a href="/agent-governance-scorecard/">Agent Governance Scorecard</a> — 30 yes/no questions across the four dimensions above. It takes about ten minutes and tells you which layer to fix first.</p>
<hr>
<p>These aren&rsquo;t theoretical patterns. They&rsquo;re the same techniques I apply when working with early-stage teams to formalize their agent architecture.</p>
<p><strong>Most early-stage teams I talk to have agents in production and governance that&rsquo;s still catching up.</strong> The gap between &ldquo;it works&rdquo; and &ldquo;I can explain why it did that&rdquo; is where real risk lives — and it&rsquo;s where investors, partners, and your first enterprise customer will start asking questions. If that sounds familiar, <a href="/pages/meet/">book a free 30-minute strategy call</a>. I&rsquo;ll walk through your agent architecture, identify the highest-risk tool surfaces, and give you a prioritized action plan: what to lock down first, what can wait, and which of these patterns fits your system. No slide decks. Just a concrete roadmap you can start executing the same week.</p>
<h2 id="further-reading">Further Reading</h2>
<ul>
<li><a href="https://hoolahoop.io/articles/cto-coaching/agentic-ai-governance/">Agentic AI Governance: What CTOs Need to Know</a> — a solid overview of the organizational and strategic side of agent governance</li>
<li><a href="https://mastra.ai/docs/agents/agent-approval">Mastra Agent Approval</a> and <a href="https://mastra.ai/docs/agents/guardrails">Guardrails</a> — the framework primitives used in the examples above</li>
<li><a href="https://www.anthropic.com/engineering/claude-code-auto-mode">Claude Code Auto Mode: A Safer Way to Skip Permissions</a> on how Anthropic built tiered approval into Claude Code</li>
<li><a href="/posts/2025-11-06-build-efficient-mcp-servers-three-design-principles/">Build Efficient MCP Servers: Three Design Principles</a> on scoping what agents can access at the tool design level</li>
<li><a href="/posts/2026-02-17-how-ai-agents-remember-things/">How AI Agents Remember Things</a> on the memory and context systems that feed agent decisions</li>
<li><a href="/posts/2026-03-25-four-patterns-that-separate-agent-ready-codebases/">Four Dimensions of Agent-Ready Codebase Design</a> on building codebases that support reliable agent output</li>
<li><a href="/posts/2026-02-05-mcps-vs-agent-skills/">MCPs vs Agent Skills</a> on architecture decisions that shape agent capabilities</li>
</ul>
]]></content:encoded></item><item><title>The Observability Layer Your AI Agent Is Missing</title><link>https://www.damiangalarza.com/videos/2026-04-14-the-observability-layer-your-ai-agent-is-missing/</link><pubDate>Tue, 14 Apr 2026 14:00:00 +0000</pubDate><author>Damian Galarza</author><guid>https://www.damiangalarza.com/videos/2026-04-14-the-observability-layer-your-ai-agent-is-missing/</guid><description>Logs tell you what happened. Traces tell you why. The three layers of agent observability, and where silent failures actually live.</description><content:encoded><![CDATA[<p>Logs tell you what happened. Traces tell you why. The three layers of agent observability, and where silent failures actually live.</p>
<p>I walk through a real production failure from my own system. My business ops agent confidently reported a completed task it had silently failed. Logs were clean. The dashboard was green. A single trace showed exactly why. This is Part 2 of the Agent Quality series, based on Google&rsquo;s Agent Quality white paper.</p>
]]></content:encoded></item><item><title>AI Agent Evals: The 4 Layers Most Teams Skip</title><link>https://www.damiangalarza.com/videos/2026-04-07-ai-agent-evals-the-4-layers-most-teams-skip/</link><pubDate>Tue, 07 Apr 2026 14:00:34 +0000</pubDate><author>Damian Galarza</author><guid>https://www.damiangalarza.com/videos/2026-04-07-ai-agent-evals-the-4-layers-most-teams-skip/</guid><description>Most teams evaluate AI agents by vibes. Here are the four layers of evals you actually need to ship agents with confidence.</description><content:encoded><![CDATA[<p>Most teams evaluate AI agents by vibes. Here are the four layers of evals you actually need to ship agents with confidence.</p>
<p>I walk through the eval stack I use on real agent projects — from unit-level prompt checks up through end-to-end trajectory scoring — and explain where each layer catches different classes of failure. If you&rsquo;re building agents for production and wondering why regressions keep slipping through, this is the framework to borrow.</p>
]]></content:encoded></item><item><title>I Gave My AI Agent Access to My Second Brain</title><link>https://www.damiangalarza.com/videos/2026-03-31-i-gave-my-ai-agent-access-to-my-second-brain/</link><pubDate>Tue, 31 Mar 2026 14:00:00 +0000</pubDate><author>Damian Galarza</author><guid>https://www.damiangalarza.com/videos/2026-03-31-i-gave-my-ai-agent-access-to-my-second-brain/</guid><description>What happens when you wire an AI agent directly into your Obsidian vault? Here's the setup I use to turn notes into real leverage.</description><content:encoded><![CDATA[<p>What happens when you wire an AI agent directly into your Obsidian vault? Here&rsquo;s the setup I use to turn notes into real leverage.</p>
<p>I walk through how I connected my second brain to an AI agent, the structure that makes retrieval actually work, and the workflows this unlocks — from daily briefings to content drafting off years of captured thinking.</p>
]]></content:encoded></item><item><title>How I Built a Personal AI Assistant with Mastra</title><link>https://www.damiangalarza.com/posts/2026-03-06-build-personal-ai-assistant/</link><pubDate>Fri, 06 Mar 2026 00:00:00 -0500</pubDate><author>Damian Galarza</author><guid>https://www.damiangalarza.com/posts/2026-03-06-build-personal-ai-assistant/</guid><description>A practical guide to building an AI agent with Mastra that researches contacts, schedules follow-ups, integrates with Slack, and uses layered memory.</description><content:encoded><![CDATA[<p>Most &ldquo;AI assistants&rdquo; are just chatbots with a context window. Ask them something, get an answer. Ask again later, and they have no idea who you are.</p>
<p>That&rsquo;s not an assistant. That&rsquo;s a search engine with a personality.</p>
<p>I wanted something different. I wanted an agent that:</p>
<ul>
<li>Researches people before my meetings</li>
<li>Reminds me to follow up</li>
<li>Remembers context across conversations</li>
<li>Acts on its own when events happen</li>
</ul>
<p>So I built one. Here&rsquo;s how it works.</p>
<h2 id="the-goal">The Goal</h2>
<p>I meet with a lot of people: founders, potential clients, partners. Before each meeting, I want to know who I&rsquo;m talking to: their background, their company, what they&rsquo;ve been working on. After each meeting, I follow up at the right time.</p>
<p>Doing this manually doesn&rsquo;t scale. I needed an agent that handles it for me.</p>
<h2 id="architecture-overview">Architecture Overview</h2>
<p>The system has five main components:</p>
<ol>
<li><strong>Communication</strong>: Slack as the primary interface, built on a platform-agnostic SDK</li>
<li><strong>Tools</strong>: Composable functions for research, scheduling, and data retrieval</li>
<li><strong>Memory</strong>: Four types (message history, semantic recall, working memory, observational) so the agent remembers across conversations</li>
<li><strong>Webhooks</strong>: Event-driven triggers that let the agent react to Cal.com bookings automatically</li>
<li><strong>Task Scheduling</strong>: Time-delayed task execution for follow-ups and reminders</li>
</ol>
<p>Each component does one thing. Together, they form an agent that acts, remembers, and follows up.</p>
<h2 id="1-tools-what-the-agent-can-do">1. Tools: What the Agent Can Do</h2>
<p>Tools are the agent&rsquo;s capabilities. Each tool is a function the agent can call when it decides to use it.</p>
<div class="highlight"><pre tabindex="0" style="color:#cdd6f4;background-color:#1e1e2e;-moz-tab-size:2;-o-tab-size:2;tab-size:2;"><code class="language-typescript" data-lang="typescript"><span style="display:flex;"><span><span style="color:#6c7086;font-style:italic">// src/mastra/tools/cal-com.ts
</span></span></span><span style="display:flex;"><span><span style="color:#6c7086;font-style:italic"></span><span style="color:#cba6f7">import</span> { createTool } <span style="color:#cba6f7">from</span> <span style="color:#a6e3a1">&#34;@mastra/core/tools&#34;</span>;
</span></span><span style="display:flex;"><span><span style="color:#cba6f7">import</span> { z } <span style="color:#cba6f7">from</span> <span style="color:#a6e3a1">&#34;zod&#34;</span>;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#cba6f7">export</span> <span style="color:#cba6f7">const</span> getUpcomingEvents <span style="color:#89dceb;font-weight:bold">=</span> createTool({
</span></span><span style="display:flex;"><span>  id<span style="color:#89dceb;font-weight:bold">:</span> <span style="color:#a6e3a1">&#34;getUpcomingEvents&#34;</span>,
</span></span><span style="display:flex;"><span>  description<span style="color:#89dceb;font-weight:bold">:</span> <span style="color:#a6e3a1">&#34;Get upcoming meetings from Cal.com for the specified date range&#34;</span>,
</span></span><span style="display:flex;"><span>  inputSchema: <span style="color:#f38ba8">z.object</span>({
</span></span><span style="display:flex;"><span>    startDate: <span style="color:#f38ba8">z.string</span>(),
</span></span><span style="display:flex;"><span>    endDate: <span style="color:#f38ba8">z.string</span>(),
</span></span><span style="display:flex;"><span>  }),
</span></span><span style="display:flex;"><span>  outputSchema: <span style="color:#f38ba8">z.object</span>({
</span></span><span style="display:flex;"><span>    title: <span style="color:#f38ba8">z.string</span>(),
</span></span><span style="display:flex;"><span>    startTime: <span style="color:#f38ba8">z.string</span>(),
</span></span><span style="display:flex;"><span>    endTime: <span style="color:#f38ba8">z.string</span>(),
</span></span><span style="display:flex;"><span>    attendees: <span style="color:#f38ba8">z.array</span>(z.<span style="color:#f38ba8">string</span>()),
</span></span><span style="display:flex;"><span>  }),
</span></span><span style="display:flex;"><span>  execute: <span style="color:#f38ba8">async</span> ({ startDate, endDate }) <span style="color:#89dceb;font-weight:bold">=&gt;</span> {
</span></span><span style="display:flex;"><span>    <span style="color:#cba6f7">const</span> events <span style="color:#89dceb;font-weight:bold">=</span> <span style="color:#cba6f7">await</span> fetchCalComEvents(startDate, endDate);
</span></span><span style="display:flex;"><span>    <span style="color:#cba6f7">return</span> events.map((e) <span style="color:#89dceb;font-weight:bold">=&gt;</span> ({
</span></span><span style="display:flex;"><span>      title: <span style="color:#f38ba8">e.title</span>,
</span></span><span style="display:flex;"><span>      startTime: <span style="color:#f38ba8">e.start</span>,
</span></span><span style="display:flex;"><span>      endTime: <span style="color:#f38ba8">e.end</span>,
</span></span><span style="display:flex;"><span>      attendees: <span style="color:#f38ba8">e.attendees.map</span>((a) <span style="color:#89dceb;font-weight:bold">=&gt;</span> a.email),
</span></span><span style="display:flex;"><span>    }));
</span></span><span style="display:flex;"><span>  },
</span></span><span style="display:flex;"><span>});
</span></span></code></pre></div><p>The tool description is the contract. The agent reads the description and decides when to call the tool. Clear descriptions = reliable tool usage.</p>
<p>I defined tools for Cal.com (scheduling), Exa (web research), and Slack (messaging):</p>
<table>
  <thead>
      <tr>
          <th>Tool</th>
          <th>What it does</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><code>getUpcomingEvents</code></td>
          <td>Fetch meetings from Cal.com</td>
      </tr>
      <tr>
          <td><code>searchVault</code></td>
          <td>Search my contacts and notes</td>
      </tr>
      <tr>
          <td><code>researchPerson</code></td>
          <td>Research a person/company via Exa</td>
      </tr>
      <tr>
          <td><code>postToSlack</code></td>
          <td>Post to the assistant channel</td>
      </tr>
      <tr>
          <td><code>scheduleTask</code></td>
          <td>Schedule a follow-up task</td>
      </tr>
  </tbody>
</table>
<p>Each tool does one thing. Simple, composable functions the agent can combine.</p>
<h2 id="2-memory-the-differentiator">2. Memory: The Differentiator</h2>
<p>Most agents have no memory. Ask them something, they answer. Ask again later, they start fresh.</p>
<p>In <a href="/posts/2026-02-17-how-ai-agents-remember-things/">How AI Agents Remember Things</a>, I covered the conceptual taxonomy: episodic memory for events and interactions, semantic memory for stable facts and preferences. Mastra maps those concepts to four concrete memory types you configure when building an agent.</p>
<table>
  <thead>
      <tr>
          <th>Mastra Type</th>
          <th>What it does</th>
          <th>Conceptual equivalent</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><strong>Message history</strong></td>
          <td>Keeps recent messages in context within a conversation</td>
          <td>Episodic (in-session)</td>
      </tr>
      <tr>
          <td><strong>Semantic recall</strong></td>
          <td>Retrieves relevant messages from past conversations by meaning</td>
          <td>Episodic (cross-session)</td>
      </tr>
      <tr>
          <td><strong>Working memory</strong></td>
          <td>Persistent structured data: your name, preferences, goals</td>
          <td>Semantic (stable facts)</td>
      </tr>
      <tr>
          <td><strong>Observational memory</strong></td>
          <td>Background summarization to keep the context window small over time</td>
          <td>Session compaction</td>
      </tr>
  </tbody>
</table>
<p><strong>Message history</strong> (<code>lastMessages</code>) is the short-term layer. It keeps the last N messages in context so the agent can follow the conversation thread. The agent can reference something you said three messages ago without you repeating it. Ten messages works well for most conversational flows.</p>
<p><strong>Semantic recall</strong> is the long-term retrieval layer. It uses vector embeddings to search across all past conversations by meaning, not keywords. When you say &ldquo;remember that thing about the Cal.com integration,&rdquo; the agent encodes your query into a vector and finds the closest matches from past messages. You configure <code>topK</code> (how many matches to retrieve) and <code>messageRange</code> (how many surrounding messages to include for context). I used LibSQL for the vector store and FastEmbed for local embeddings, so the entire pipeline runs without external API calls.</p>
<p><strong>Working memory</strong> is the persistent layer. It&rsquo;s a structured scratchpad the agent updates over time as it learns about you. Unlike message history and semantic recall, which store raw messages, working memory stores distilled facts: your name, your role, your preferences. You define a template, and the agent fills it in as it picks up information from conversations. This is what makes the agent feel like it knows you, even in a brand new thread.</p>
<p><strong>Observational memory</strong> uses background Observer and Reflector agents to maintain a dense observation log that replaces raw message history as it grows. I haven&rsquo;t wired this up yet, but it solves the context window problem: as conversations get long, you can&rsquo;t keep everything in context. Observational memory compresses it down without losing the long-term thread.</p>
<p>Here&rsquo;s how the first three are configured:</p>
<div class="highlight"><pre tabindex="0" style="color:#cdd6f4;background-color:#1e1e2e;-moz-tab-size:2;-o-tab-size:2;tab-size:2;"><code class="language-typescript" data-lang="typescript"><span style="display:flex;"><span><span style="color:#6c7086;font-style:italic">// src/mastra/agents/meeting-assistant.ts
</span></span></span><span style="display:flex;"><span><span style="color:#6c7086;font-style:italic"></span><span style="color:#cba6f7">import</span> { Agent } <span style="color:#cba6f7">from</span> <span style="color:#a6e3a1">&#39;@mastra/core/agent&#39;</span>;
</span></span><span style="display:flex;"><span><span style="color:#cba6f7">import</span> { Memory } <span style="color:#cba6f7">from</span> <span style="color:#a6e3a1">&#39;@mastra/memory&#39;</span>;
</span></span><span style="display:flex;"><span><span style="color:#cba6f7">import</span> { LibSQLVector } <span style="color:#cba6f7">from</span> <span style="color:#a6e3a1">&#39;@mastra/libsql&#39;</span>;
</span></span><span style="display:flex;"><span><span style="color:#cba6f7">import</span> { fastembed } <span style="color:#cba6f7">from</span> <span style="color:#a6e3a1">&#39;@mastra/fastembed&#39;</span>;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#cba6f7">const</span> memory <span style="color:#89dceb;font-weight:bold">=</span> <span style="color:#cba6f7">new</span> Memory({
</span></span><span style="display:flex;"><span>  <span style="color:#6c7086;font-style:italic">// Vector store for semantic recall
</span></span></span><span style="display:flex;"><span><span style="color:#6c7086;font-style:italic"></span>  vector: <span style="color:#f38ba8">new</span> LibSQLVector({
</span></span><span style="display:flex;"><span>    id<span style="color:#89dceb;font-weight:bold">:</span> <span style="color:#a6e3a1">&#34;memory-vector&#34;</span>,
</span></span><span style="display:flex;"><span>    url<span style="color:#89dceb;font-weight:bold">:</span> <span style="color:#a6e3a1">&#34;file:./mastra.db&#34;</span>,
</span></span><span style="display:flex;"><span>  }),
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>  <span style="color:#6c7086;font-style:italic">// Local embedding model, no API key needed
</span></span></span><span style="display:flex;"><span><span style="color:#6c7086;font-style:italic"></span>  embedder: <span style="color:#f38ba8">fastembed</span>,
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>  options<span style="color:#89dceb;font-weight:bold">:</span> {
</span></span><span style="display:flex;"><span>    <span style="color:#6c7086;font-style:italic">// Message history: keeps the last 10 messages in context
</span></span></span><span style="display:flex;"><span><span style="color:#6c7086;font-style:italic"></span>    lastMessages: <span style="color:#f38ba8">10</span>,
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#6c7086;font-style:italic">// Semantic recall: searches past conversations by meaning
</span></span></span><span style="display:flex;"><span><span style="color:#6c7086;font-style:italic"></span>    semanticRecall<span style="color:#89dceb;font-weight:bold">:</span> {
</span></span><span style="display:flex;"><span>      topK: <span style="color:#f38ba8">3</span>,
</span></span><span style="display:flex;"><span>      messageRange: <span style="color:#f38ba8">2</span>,
</span></span><span style="display:flex;"><span>    },
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#6c7086;font-style:italic">// Working memory: persistent user profile the agent updates over time
</span></span></span><span style="display:flex;"><span><span style="color:#6c7086;font-style:italic"></span>    workingMemory<span style="color:#89dceb;font-weight:bold">:</span> {
</span></span><span style="display:flex;"><span>      enabled: <span style="color:#f38ba8">true</span>,
</span></span><span style="display:flex;"><span>      template<span style="color:#89dceb;font-weight:bold">:</span> <span style="color:#a6e3a1">`# User Profile
</span></span></span><span style="display:flex;"><span><span style="color:#a6e3a1">- Name:
</span></span></span><span style="display:flex;"><span><span style="color:#a6e3a1">- Role:
</span></span></span><span style="display:flex;"><span><span style="color:#a6e3a1">- Company:
</span></span></span><span style="display:flex;"><span><span style="color:#a6e3a1">- Communication style:
</span></span></span><span style="display:flex;"><span><span style="color:#a6e3a1">- Meeting prep preferences:
</span></span></span><span style="display:flex;"><span><span style="color:#a6e3a1">`</span>,
</span></span><span style="display:flex;"><span>    },
</span></span><span style="display:flex;"><span>  },
</span></span><span style="display:flex;"><span>});
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#cba6f7">export</span> <span style="color:#cba6f7">const</span> meetingAssistant <span style="color:#89dceb;font-weight:bold">=</span> <span style="color:#cba6f7">new</span> Agent({
</span></span><span style="display:flex;"><span>  name<span style="color:#89dceb;font-weight:bold">:</span> <span style="color:#a6e3a1">&#39;MeetingAssistant&#39;</span>,
</span></span><span style="display:flex;"><span>  model<span style="color:#89dceb;font-weight:bold">:</span> <span style="color:#a6e3a1">&#39;openai/gpt-4.1&#39;</span>,
</span></span><span style="display:flex;"><span>  memory,
</span></span><span style="display:flex;"><span>});
</span></span></code></pre></div><p>Mastra handles storage, retrieval, and injection into the agent&rsquo;s context automatically. You configure the types declaratively; the framework does the rest.</p>
<h2 id="3-slack-integration">3. Slack Integration</h2>
<p>Communication happens through Slack via the <a href="https://chat-sdk.dev/">Chat SDK</a>, a platform-agnostic interface for bot communication.</p>
<div class="highlight"><pre tabindex="0" style="color:#cdd6f4;background-color:#1e1e2e;-moz-tab-size:2;-o-tab-size:2;tab-size:2;"><code class="language-typescript" data-lang="typescript"><span style="display:flex;"><span><span style="color:#6c7086;font-style:italic">// src/chat.ts
</span></span></span><span style="display:flex;"><span><span style="color:#6c7086;font-style:italic"></span><span style="color:#cba6f7">import</span> { Chat } <span style="color:#cba6f7">from</span> <span style="color:#a6e3a1">&#34;chat&#34;</span>;
</span></span><span style="display:flex;"><span><span style="color:#cba6f7">import</span> { createSlackAdapter } <span style="color:#cba6f7">from</span> <span style="color:#a6e3a1">&#34;@chat-adapter/slack&#34;</span>;
</span></span><span style="display:flex;"><span><span style="color:#cba6f7">import</span> { meetingAssistant } <span style="color:#cba6f7">from</span> <span style="color:#a6e3a1">&#34;./mastra/agents/meeting-assistant&#34;</span>;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#cba6f7">export</span> <span style="color:#cba6f7">const</span> bot <span style="color:#89dceb;font-weight:bold">=</span> <span style="color:#cba6f7">new</span> Chat({
</span></span><span style="display:flex;"><span>  userName<span style="color:#89dceb;font-weight:bold">:</span> <span style="color:#a6e3a1">&#34;meeting-assistant&#34;</span>,
</span></span><span style="display:flex;"><span>  adapters<span style="color:#89dceb;font-weight:bold">:</span> {
</span></span><span style="display:flex;"><span>    slack: <span style="color:#f38ba8">createSlackAdapter</span>(),
</span></span><span style="display:flex;"><span>  },
</span></span><span style="display:flex;"><span>});
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>bot.onNewMention(<span style="color:#cba6f7">async</span> (thread, message) <span style="color:#89dceb;font-weight:bold">=&gt;</span> {
</span></span><span style="display:flex;"><span>  <span style="color:#cba6f7">await</span> thread.subscribe();
</span></span><span style="display:flex;"><span>  <span style="color:#cba6f7">await</span> thread.startTyping();
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>  <span style="color:#cba6f7">const</span> result <span style="color:#89dceb;font-weight:bold">=</span> <span style="color:#cba6f7">await</span> meetingAssistant.generate(message.text, {
</span></span><span style="display:flex;"><span>    memory<span style="color:#89dceb;font-weight:bold">:</span> {
</span></span><span style="display:flex;"><span>      thread: <span style="color:#f38ba8">thread.id</span>,
</span></span><span style="display:flex;"><span>      resource<span style="color:#89dceb;font-weight:bold">:</span> <span style="color:#a6e3a1">&#34;user&#34;</span>,
</span></span><span style="display:flex;"><span>    },
</span></span><span style="display:flex;"><span>  });
</span></span><span style="display:flex;"><span>  <span style="color:#cba6f7">await</span> thread.post(result.text);
</span></span><span style="display:flex;"><span>});
</span></span></code></pre></div><p>Two things happening here. First, <code>onNewMention</code> fires when someone @mentions the bot in Slack. Second, <code>memory.thread</code> scopes messages to the specific Slack thread, while <code>memory.resource</code> uses a fixed ID so working memory (your profile) is shared across all threads.</p>
<h2 id="4-webhooks-reacting-to-events">4. Webhooks: Reacting to Events</h2>
<p>The agent needs to know when a meeting is booked. That&rsquo;s where webhooks come in.</p>
<div class="highlight"><pre tabindex="0" style="color:#cdd6f4;background-color:#1e1e2e;-moz-tab-size:2;-o-tab-size:2;tab-size:2;"><code class="language-typescript" data-lang="typescript"><span style="display:flex;"><span><span style="color:#6c7086;font-style:italic">// src/mastra/index.ts
</span></span></span><span style="display:flex;"><span><span style="color:#6c7086;font-style:italic"></span><span style="color:#cba6f7">import</span> { Mastra } <span style="color:#cba6f7">from</span> <span style="color:#a6e3a1">&#34;@mastra/core&#34;</span>;
</span></span><span style="display:flex;"><span><span style="color:#cba6f7">import</span> { registerApiRoute } <span style="color:#cba6f7">from</span> <span style="color:#a6e3a1">&#34;@mastra/core/server&#34;</span>;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#cba6f7">export</span> <span style="color:#cba6f7">const</span> mastra <span style="color:#89dceb;font-weight:bold">=</span> <span style="color:#cba6f7">new</span> Mastra({
</span></span><span style="display:flex;"><span>  server<span style="color:#89dceb;font-weight:bold">:</span> {
</span></span><span style="display:flex;"><span>    apiRoutes<span style="color:#89dceb;font-weight:bold">:</span> [
</span></span><span style="display:flex;"><span>      registerApiRoute(<span style="color:#a6e3a1">&#34;/webhooks/cal&#34;</span>, {
</span></span><span style="display:flex;"><span>        method<span style="color:#89dceb;font-weight:bold">:</span> <span style="color:#a6e3a1">&#34;POST&#34;</span>,
</span></span><span style="display:flex;"><span>        handler: <span style="color:#f38ba8">async</span> (c) <span style="color:#89dceb;font-weight:bold">=&gt;</span> {
</span></span><span style="display:flex;"><span>          <span style="color:#cba6f7">const</span> payload <span style="color:#89dceb;font-weight:bold">=</span> <span style="color:#cba6f7">await</span> c.req.json();
</span></span><span style="display:flex;"><span>          <span style="color:#cba6f7">const</span> triggerEvent <span style="color:#89dceb;font-weight:bold">=</span> payload.triggerEvent;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>          <span style="color:#cba6f7">if</span> (triggerEvent <span style="color:#89dceb;font-weight:bold">!==</span> <span style="color:#a6e3a1">&#34;BOOKING_CREATED&#34;</span>) {
</span></span><span style="display:flex;"><span>            <span style="color:#cba6f7">return</span> c.json({ ok: <span style="color:#f38ba8">true</span>, skipped: <span style="color:#f38ba8">true</span> });
</span></span><span style="display:flex;"><span>          }
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>          <span style="color:#cba6f7">const</span> attendee <span style="color:#89dceb;font-weight:bold">=</span> payload.payload<span style="color:#89dceb;font-weight:bold">?</span>.attendees<span style="color:#89dceb;font-weight:bold">?</span>.[<span style="color:#fab387">0</span>];
</span></span><span style="display:flex;"><span>          <span style="color:#cba6f7">const</span> channel <span style="color:#89dceb;font-weight:bold">=</span> bot.channel(<span style="color:#a6e3a1">`slack:</span><span style="color:#a6e3a1">${</span>process.env.SLACK_CHANNEL_ID<span style="color:#a6e3a1">}</span><span style="color:#a6e3a1">`</span>);
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>          <span style="color:#6c7086;font-style:italic">// Post immediately, then research asynchronously
</span></span></span><span style="display:flex;"><span><span style="color:#6c7086;font-style:italic"></span>          channel.post(<span style="color:#a6e3a1">`Researching *</span><span style="color:#a6e3a1">${</span>attendee.name<span style="color:#a6e3a1">}</span><span style="color:#a6e3a1">* for upcoming meeting...`</span>).then(<span style="color:#cba6f7">async</span> (sent) <span style="color:#89dceb;font-weight:bold">=&gt;</span> {
</span></span><span style="display:flex;"><span>            <span style="color:#cba6f7">const</span> threadId <span style="color:#89dceb;font-weight:bold">=</span> <span style="color:#a6e3a1">`slack:</span><span style="color:#a6e3a1">${</span>channelId<span style="color:#a6e3a1">}</span><span style="color:#a6e3a1">:</span><span style="color:#a6e3a1">${</span>sent.id<span style="color:#a6e3a1">}</span><span style="color:#a6e3a1">`</span>;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>            <span style="color:#cba6f7">const</span> prompt <span style="color:#89dceb;font-weight:bold">=</span> [
</span></span><span style="display:flex;"><span>              <span style="color:#a6e3a1">`I have a meeting coming up with </span><span style="color:#a6e3a1">${</span>attendee.name<span style="color:#a6e3a1">}</span><span style="color:#a6e3a1"> (</span><span style="color:#a6e3a1">${</span>attendee.email<span style="color:#a6e3a1">}</span><span style="color:#a6e3a1">).`</span>,
</span></span><span style="display:flex;"><span>              <span style="color:#a6e3a1">`Event: </span><span style="color:#a6e3a1">${</span>payload.payload.title<span style="color:#a6e3a1">}</span><span style="color:#a6e3a1">`</span>,
</span></span><span style="display:flex;"><span>              <span style="color:#a6e3a1">`Time: </span><span style="color:#a6e3a1">${</span>payload.payload.startTime<span style="color:#a6e3a1">}</span><span style="color:#a6e3a1">`</span>,
</span></span><span style="display:flex;"><span>              <span style="color:#a6e3a1">`Research this person and give me a concise meeting brief.`</span>,
</span></span><span style="display:flex;"><span>            ].join(<span style="color:#a6e3a1">&#34;\n&#34;</span>);
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>            <span style="color:#cba6f7">const</span> result <span style="color:#89dceb;font-weight:bold">=</span> <span style="color:#cba6f7">await</span> meetingAssistant.generate(prompt);
</span></span><span style="display:flex;"><span>            <span style="color:#cba6f7">await</span> slack.postMessage(threadId, { markdown: <span style="color:#f38ba8">result.text</span> });
</span></span><span style="display:flex;"><span>          });
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>          <span style="color:#cba6f7">return</span> c.json({ ok: <span style="color:#f38ba8">true</span> });
</span></span><span style="display:flex;"><span>        },
</span></span><span style="display:flex;"><span>      }),
</span></span><span style="display:flex;"><span>    ],
</span></span><span style="display:flex;"><span>  },
</span></span><span style="display:flex;"><span>});
</span></span></code></pre></div><p>The webhook receives the Cal.com booking payload, immediately posts a &ldquo;researching&rdquo; message to Slack, then kicks off the agent to research and post the brief. This way Cal.com doesn&rsquo;t time out waiting for the research to complete.</p>
<h2 id="5-task-scheduling-time-delayed-actions">5. Task Scheduling: Time-Delayed Actions</h2>
<p>After a meeting, the agent should follow up. That requires scheduling a task for later execution.</p>
<div class="highlight"><pre tabindex="0" style="color:#cdd6f4;background-color:#1e1e2e;-moz-tab-size:2;-o-tab-size:2;tab-size:2;"><code class="language-typescript" data-lang="typescript"><span style="display:flex;"><span><span style="color:#6c7086;font-style:italic">// src/scheduler.ts
</span></span></span><span style="display:flex;"><span><span style="color:#6c7086;font-style:italic"></span><span style="color:#cba6f7">export</span> <span style="color:#cba6f7">async</span> <span style="color:#f38ba8">function</span> scheduleTask(
</span></span><span style="display:flex;"><span>  name: <span style="color:#f38ba8">string</span>,
</span></span><span style="display:flex;"><span>  <span style="color:#cba6f7">type</span><span style="color:#89dceb;font-weight:bold">:</span> <span style="color:#f38ba8">string</span>,
</span></span><span style="display:flex;"><span>  scheduledFor: <span style="color:#f38ba8">string</span>,
</span></span><span style="display:flex;"><span>  payload: <span style="color:#f38ba8">Record</span>&lt;<span style="color:#cba6f7">string</span>, <span style="color:#89b4fa">unknown</span>&gt;,
</span></span><span style="display:flex;"><span>) {
</span></span><span style="display:flex;"><span>  <span style="color:#cba6f7">await</span> db.insert(scheduledTasks).values({
</span></span><span style="display:flex;"><span>    name,
</span></span><span style="display:flex;"><span>    <span style="color:#cba6f7">type</span>,
</span></span><span style="display:flex;"><span>    scheduledFor,
</span></span><span style="display:flex;"><span>    payload: <span style="color:#f38ba8">JSON.stringify</span>(payload),
</span></span><span style="display:flex;"><span>  });
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>The scheduler polls every 30 seconds for due tasks, marks them as running, executes the handler, then marks them complete or failed. Simple, reliable, no external dependencies.</p>
<div class="highlight"><pre tabindex="0" style="color:#cdd6f4;background-color:#1e1e2e;-moz-tab-size:2;-o-tab-size:2;tab-size:2;"><code class="language-typescript" data-lang="typescript"><span style="display:flex;"><span>registerTaskHandler(<span style="color:#a6e3a1">&#34;follow-up&#34;</span>, <span style="color:#cba6f7">async</span> (payload) <span style="color:#89dceb;font-weight:bold">=&gt;</span> {
</span></span><span style="display:flex;"><span>  <span style="color:#cba6f7">const</span> { threadId, message } <span style="color:#89dceb;font-weight:bold">=</span> payload <span style="color:#cba6f7">as</span> { threadId: <span style="color:#f38ba8">string</span>; message: <span style="color:#f38ba8">string</span> };
</span></span><span style="display:flex;"><span>  <span style="color:#cba6f7">const</span> slack <span style="color:#89dceb;font-weight:bold">=</span> bot.getAdapter(<span style="color:#a6e3a1">&#34;slack&#34;</span>);
</span></span><span style="display:flex;"><span>  <span style="color:#cba6f7">await</span> slack.postMessage(threadId, { markdown: <span style="color:#f38ba8">message</span> });
</span></span><span style="display:flex;"><span>});
</span></span></code></pre></div><p>When the meeting ends, the webhook handler schedules a follow-up task. Thirty seconds after the scheduled end time, the handler fires and posts to the Slack thread: &ldquo;The meeting should be wrapping up! How did it go?&rdquo;</p>
<h2 id="the-mental-model">The Mental Model</h2>
<p>Here&rsquo;s how I think about agent architecture now:</p>
<p><strong>Communication</strong> is the interface. Slack, Telegram, or any messaging platform. It&rsquo;s just how the user talks to the agent.</p>
<p><strong>Tools</strong> are the agent&rsquo;s capabilities. Each tool should do one thing well. The description is the contract. Write it clearly, or the agent won&rsquo;t know when to use it.</p>
<p><strong>Memory</strong> is the differentiator. Message history for in-session continuity, semantic recall for cross-session retrieval, working memory for persistent user facts. Most agents fail because they only implement one or none.</p>
<p><strong>Webhooks</strong> make it proactive. Without external triggers, the agent only acts when asked. With webhooks, it can act on events: bookings, form submissions, anything.</p>
<p><strong>Task scheduling</strong> closes the loop. The agent doesn&rsquo;t just respond. It reminds, follows up, checks in. Time-delayed actions turn a reactive chatbot into a proactive assistant.</p>
<h2 id="the-point">The Point</h2>
<p>The point isn&rsquo;t the code. It&rsquo;s the architecture.</p>
<p>An agent that only chats doesn&rsquo;t get you very far. An agent that integrates with your tools, remembers across conversations, reacts to events, and follows up on time is actually useful.</p>
<p>Mastra makes this easier than rolling your own. But the pattern works with any framework: give the agent tools, give it memory in layers, connect it to events, and let it act on time.</p>
<p>That&rsquo;s an assistant. Everything else is a chatbot.</p>
<h2 id="code">Code</h2>
<p>The full implementation is on GitHub: <a href="https://github.com/dgalarza/mastra-meeting-assistant">github.com/dgalarza/mastra-meeting-assistant</a></p>
<h2 id="want-help-building-this">Want Help Building This?</h2>
<p>If you&rsquo;re building AI agents into your workflow, whether it&rsquo;s a personal assistant, an internal tool, or a customer-facing product, <a href="/ai-agents">I can help</a>.</p>
<h2 id="further-reading">Further Reading</h2>
<ul>
<li><a href="https://mastra.ai/docs/">Mastra Documentation</a></li>
<li><a href="https://mastra.ai/docs/memory/overview">Mastra Memory</a></li>
<li><a href="https://chat-sdk.dev/">Chat SDK</a></li>
<li><a href="/posts/2026-02-17-how-ai-agents-remember-things/">How AI Agents Remember Things</a>: deep dive into the memory taxonomy and architecture behind agents that remember</li>
<li><a href="https://youtu.be/Seu7nksZ_4k?si=Xx8wnlL6j8nsLYq5">How AI Agents Remember Things (YouTube)</a>: video walkthrough of agent memory systems</li>
<li><a href="https://docs.exa.ai/">Exa API</a></li>
</ul>
]]></content:encoded></item><item><title>Build Your Own AI Agent from Scratch (Mastra + TypeScript)</title><link>https://www.damiangalarza.com/videos/2026-03-05-build-your-own-ai-agent-from-scratch-mastra-typescript/</link><pubDate>Thu, 05 Mar 2026 17:01:18 +0000</pubDate><author>Damian Galarza</author><guid>https://www.damiangalarza.com/videos/2026-03-05-build-your-own-ai-agent-from-scratch-mastra-typescript/</guid><description>Learn to build your own AI agent that actually does work for you, not just answers questions.</description><content:encoded>&lt;p>Learn to build your own AI agent that actually does work for you, not just answers questions.&lt;/p>
&lt;p>In this video, I show you the core patterns behind every useful AI agent: tools, memory, webhooks, and scheduled tasks. We build a meeting prep assistant with Mastra (TypeScript) as the example, but the patterns apply to any agent you want to build.&lt;/p>
</content:encoded></item><item><title>How AI Agents Remember Things</title><link>https://www.damiangalarza.com/videos/2026-02-11-how-ai-agents-remember-things/</link><pubDate>Wed, 11 Feb 2026 00:00:35 +0000</pubDate><author>Damian Galarza</author><guid>https://www.damiangalarza.com/videos/2026-02-11-how-ai-agents-remember-things/</guid><description>How do AI agents remember things between sessions? Every agent forgets everything when a conversation ends, so how do the best ones seem to know you?</description><content:encoded><![CDATA[<p>How do AI agents remember things between sessions? Every agent forgets everything when a conversation ends, so how do the best ones seem to know you?</p>
<p>I break down the memory architecture behind real AI agents, using OpenClaw (an open-source AI assistant) as a reference implementation. You&rsquo;ll see how LLM agents write, store, and load persistent memory using plain markdown files, and the four mechanisms that keep context across sessions, including context window management, bootstrap loading, and pre-compaction memory flush.</p>
]]></content:encoded></item><item><title>How OpenClaw Works: The Architecture Behind the 'Magic'</title><link>https://www.damiangalarza.com/videos/2026-02-04-how-openclaw-works-the-architecture-behind-the-magic/</link><pubDate>Wed, 04 Feb 2026 02:00:22 +0000</pubDate><author>Damian Galarza</author><guid>https://www.damiangalarza.com/videos/2026-02-04-how-openclaw-works-the-architecture-behind-the-magic/</guid><description>OpenClaw agent architecture explained: How autonomous AI agents like ClawdBot create the illusion of sentience using just inputs, queues, and a loop.</description><content:encoded><![CDATA[<p>OpenClaw agent architecture explained: How autonomous AI agents like ClawdBot create the illusion of sentience using just inputs, queues, and a loop.</p>
<p>(Previously known as ClawdBot / MoltBot)</p>
<p>In this deep dive, I break down the 5 input types that power OpenClaw—Messages, Heartbeats, Crons, Hooks, and Webhooks—and show you the simple formula behind agents that seem to think on their own.</p>
]]></content:encoded></item></channel></rss>