<?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>Bartosz&#39;s blog</title>
    <link>https://ocytko.net/</link>
    <description>Recent content on Bartosz&#39;s blog</description>
    <generator>Hugo -- 0.155.3</generator>
    <language>en</language>
    <copyright>Bartosz Ocytko</copyright>
    <lastBuildDate>Sat, 04 Jul 2026 21:00:00 +0000</lastBuildDate>
    <atom:link href="https://ocytko.net/index.xml" rel="self" type="application/rss+xml" />
    <item>
      <title>Looking back at the Future of Software Engineering unconference</title>
      <link>https://ocytko.net/posts/fose-2026-reflections/</link>
      <pubDate>Sat, 04 Jul 2026 21:00:00 +0000</pubDate>
      <guid>https://ocytko.net/posts/fose-2026-reflections/</guid>
      <description>Reflections on Software Factories, the You build it, you run it principle, role of IDEs - themes from the Future of Software Engineering unconference.</description>
      <content:encoded><![CDATA[<p>Recently, I attended an unconference called &ldquo;<a href="https://martinfowler.com/bliki/FutureOfSoftwareDevelopment.html">The Future of Software Engineering</a>&rdquo; hosted by Thoughtworks.
This post is a reflection on the key themes I found important and I formed a clear perspective around. Thanks to everyone at the event for the exchanges.</p>
<h2 id="software-factories">Software factories</h2>
<p>The holy grail is a self-evolving system where a feature request is magically built in the existing system. Side-effects aside, whether and when we will reach this target remains to be seen. We&rsquo;re certainly on path towards the target with teams who get a sense of this future reality in parts of their processes.</p>
<p>As teams work on codifying steps of the development process, we&rsquo;re seeing more and more proof points for reusable workflows that accelerate release cycles. A single bug or feature request can now be designed, coded, and verified in an agentic loop, if supported with the right tools, platform, and guardrails. Teams that have well-engineered systems and high engineering rigour benefit the most. It&rsquo;s all about low build times enabling fast iteration, modular architecture making code changes cheap and simple, and high test coverage enabling continuous delivery to users.</p>
<p>Better models enable multi-hour execution for increasing scope, triggering the move of agents from the local developer machines to micro VMs that host them. Leading teams can reverse-engineer existing code, design a replacement and the needed guardrails, and build loops that will eventually result in a running system that passes all checks. This approach accelerates migrations and codebase modernization. Guardrails also become more autonomous with highly specialized background agents that compensate for lower quality code generated by coding agents and that protect invariants or stability of the system. Examples include agents that refactor code, apply reliability patterns, write missing tests, reorganize test suites to reduce execution time, etc.</p>
<p>The key question remains where the developers are in this process. For the foreseeable time, they will build and operate the backbone that builds the system (thus the name <em>software factory</em>) and set the constraints under which the system is allowed to evolve. Depending on business needs and appetite for risk, teams can choose from a set of hand-off points where human interaction and sign-off is explicitly required:</p>
<ul>
<li><strong>idea-&gt;plan</strong>: explicit decision whether to act on the idea or feature request with focus on keeping the scope of the product and key user journeys in place.</li>
<li><strong>plan-&gt;execute</strong>: humans review plans or specification, increasing the likelihood of understanding the system. After approval, the system takes over implementation, verification, and more.</li>
<li><strong>change request-&gt;deploy</strong>: final pass code review, if required by regulatory needs.</li>
<li><strong>incident-&gt;remediation</strong>: the on-call person takes a decision on remediation making the needed trade-offs taking their business knowledge and current state of the system into account.</li>
</ul>
<p>These handoff points are not entirely new, yet these points seem to be a good anchor for explicitly keeping the software delivery in check. Each of the steps is prone to benefit from further automation supporting the decision making process. For change request analysis, the person can be provided with a measure of risk of the changes, a summary of the changes can be automatically tested against the specification or regulatory guardrails. To speed up incident response, the on-call person can receive an automatically collected summary of the state of the system and changes around the time of incident.</p>
<h2 id="-vs-"><em>&ldquo;You build it, you run it&rdquo;</em> vs. <em>&ldquo;They build it, you run it&rdquo;</em></h2>
<p>The <em>&ldquo;You build it, you run it&rdquo;</em> approach has promoted healthy practices in teams and increased reliability of the supported systems. With citizen developers (non-engineers who build apps with AI assistance) getting abilities to build applications, it&rsquo;s tempting to drift back to <em>&ldquo;They build it, you run it&rdquo;</em>, placing the burden of ownership and operations on an existing engineering team.</p>
<p>A question arises how we can avoid creating a culture of throwing AI-generated apps over the fence, just to be operated by experts who never wrote a single line in any of these apps? Training citizen developers to have intuition on software and security risks is going to pay off in general. Yet, requiring everyone to acquire a baseline set of skills of software engineers is not the only path. Typically, it&rsquo;s platforms that separate concerns, offering user-level abstractions that run on top of a solid foundation.</p>
<p>This foundation essentially needs to operate itself and be self-healing in terms of known failure modes, beyond simply recycling unhealthy compute nodes. For example, for applications with databases, it&rsquo;s possible to add automatic volume increase to avoid running out of space or adding an index to speed up expensive queries. Effectively, the idea is to make these failure modes disappear (in most cases), reducing the burden on the user to know about these concerns from the get go and by setting healthy constraints (e.g. increase disk space max. n times). Existing vibe coding solutions from vendors typically fall short when it comes to accessing data and APIs from production systems. Usually, this leads to static data being embedded in applications, resulting in bypassing access control for data or public leaks in the worst case. The platform can and should ship with mechanisms protecting users, setting the right access and data guardrails.</p>
<p>The challenge remains on what the starting point of the citizen developer journey is. Developers start with a repo or clone an existing template with scaffolding. Citizen developers require to be guided through the creation process from the very first prompt they write. On top, this needs to happen with as little friction as possible so that they (and the agent) do not look for workaround. Defining tight constraints enables a progressive build out of the platform and sets clear expectations to the user on what&rsquo;s possible for them to build, be it a static website, a data visualization dashboard, or a simple tool with limited access to data from internal systems.</p>
<h2 id="will-we-still-need-ides">Will we still need IDEs?</h2>
<p>Traditional IDEs ship with tools that help navigate the code base and perform deterministic code transformation. Yet, popular plugins from coding agent providers reduce the role of the IDE to a mere terminal/chat window and rendering of large change sets. Thus, it&rsquo;s easy and common to skip IDEs completely, favoring the popular coding CLIs where reviewing large plans/specs and preparing prompts is a very barebones and basic experience that surprisingly works well enough.</p>
<p>As IDE users migrate off to CLIs, the question is what features could help slow down this trend. The incumbent IDEs are clearly missing out on:</p>
<ul>
<li>exposing access to deterministic IDE tools for transformation and refactoring, lowering the token bills</li>
<li>access to the IDE environment for compilation, running tests, reducing failure rate in coding loops</li>
<li>annotating agent output with feedback (visual and text) to increase accuracy in prompting</li>
<li>native support for agent skills with support for verification and (back)testing</li>
<li>multi-session orchestration to avoid conflicting changes done by agents in parallel</li>
<li>coding session insights with usage statistics and view of agent traces</li>
</ul>
<p>Some of these features are already available via tools (e.g. <a href="https://github.com/getagentseal/codeburn">codeburn</a>, <a href="https://www.agentsview.io/">agentsview</a>, <a href="https://github.com/cased/kit">kit</a>), but bundling them into a (new) product is highly likely to improve the agentic engineering experience. Coding agents also ship with OpenTelemetry support, containing telemetry signals that can be captured and visualized. Visualizing agent session telemetry comes with new challenges related to data privacy as traditional observability tools are designed to provide broad access to application telemetry, rather than offering access to user-level data.</p>
<h2 id="growing-gaps">Growing gaps</h2>
<p>It&rsquo;s well known that agentic engineering amplifies both the bad and good practices in teams. Teams that ship in fast cycles will likely continuously widen the gap between themselves and their peers who are slow to adapt. It&rsquo;s impressive to hear from new startups that invested in codifying processes early on and how they&rsquo;re benefiting from the acceleration that agentic engineering offers.</p>
<p>Small companies benefit first as it&rsquo;s much easier for one team member to build the set of guardrails in code and shared skills. Growing from this early base helps accelerate delivery at increased consistency.</p>
<p>Existing companies face the risk of increasing fragmentation on process and tech stack, reinforcing status quo (e.g. adopting microservice architectures with a high degree of freedom of choice for the tech stack and lack of platform support and/or application level SDKs). Common approaches are encouraging the build out of shared skill marketplaces and shared tooling that supports the engineering process, yet the fate of these efforts highly depends on organisational structures and culture of re-use.</p>
<p>A future where developer platform teams step up by one layer and provide a unified process of building software is likely. However, working styles and practices differ significantly on individual level, often being part of one&rsquo;s identity and skill differentiation. Forcing large groups of engineers to adopt the same structure can become too constraining and limit innovation, even if sufficient extension points for customization are provided.</p>
<h2 id="summary">Summary</h2>
<p>During the event, we discussed many proof points for future timelines, yet many questions still remain open. While it&rsquo;s clearer where human judgement will be most needed when agentic engineering takes off and crunches through product backlogs, it&rsquo;s less clear what organisational structures and shapes of teams will prevail or correlate with successful outcomes. Unclear terms reveal how young the discipline is with the added level of complexity of social media hyping up names, without triggering prior discussions on these amongst practitioners with decades of engineering expertise.</p>
<p>The future is certainly exciting and requires leadership from practitioners to influence and help understand trade-offs along the journey we&rsquo;re on.</p>
]]></content:encoded>
    </item>
    <item>
      <title>Quo Vadis, Agentic Engineering?</title>
      <link>https://ocytko.net/posts/quo-vadis-agentic-engineering/</link>
      <pubDate>Wed, 22 Apr 2026 20:00:00 +0000</pubDate>
      <guid>https://ocytko.net/posts/quo-vadis-agentic-engineering/</guid>
      <description>The post highlights constraints, mechanisms, and factors influencing Agentic Engineering, emphasizing the types of bottlenecks we&amp;rsquo;re hitting and how GPU shortages are driving product changes.</description>
      <content:encoded><![CDATA[<p>It&rsquo;s a very eventful time for the industry. Some compare it to the most exciting period in engineering since the rise of the Internet, while others see parallels with the adoption of cloud computing and microservices.
Others already seek a manifesto moment, though in <a href="https://martinfowler.com/fragments/2026-02-18.html">Martin Fowler&rsquo;s view</a> it&rsquo;s way too early for that: in XP and Agile terms we&rsquo;re <a href="https://www.youtube.com/watch?v=VHkuVlwYhNk&amp;t=70">at the stage</a> comparable to the early 90s full of experimentation with new ideas (forming XP) and around the Workshop on Object-Oriented Design (<a href="https://wiki.c2.com/?EverythingAboutObjects">WOOD</a>) <sup id="fnref:1"><a href="#fn:1" class="footnote-ref" role="doc-noteref">1</a></sup>.</p>
<p>The sheer influx of information and day-to-day developments is really hard to follow. Just in January Steve Yegge released a new take on the IDE with <a href="https://steve-yegge.medium.com/welcome-to-gas-town-4f25ee16dd04">Gastown</a>, only to reflect a month later on being <a href="https://steve-yegge.medium.com/the-ai-vampire-eda6e4f07163">bitten by the AI vampire</a>, highlighting the <a href="https://www.fast.ai/posts/2026-01-28-dark-flow/">addictive side</a> of building software with agents. The impact of FOMO and pressure on <a href="https://x.com/tomdale/status/2019640306342457450">mental health</a> is widely discussed in the industry as well, given agents can <a href="https://theshamblog.com/an-ai-agent-published-a-hit-piece-on-me/">amplify undesired behavior</a>. OpenClaw triggered euphoria, showing the power of agents to a wide audience, further accelerating the FOMO. In the meantime, its author Peter Steinberger was <a href="https://steipete.me/posts/2026/openclaw">acquihired by OpenAI</a>, with the project transitioning into a foundation (see <a href="https://www.youtube.com/watch?v=zgNvts_2TUE">State of the Claw</a> for a recent update). Others claim not to be fooled again, <a href="https://www.flyingpenguin.com/build-an-openclaw-free-secure-always-on-local-ai-agent/">recollecting MS-DOS times</a>.</p>
<p>The key question is where things are going medium to long term, hence the <a href="https://www.britannica.com/topic/Quo-Vadis-novel">title of the post</a>.
My goal is to highlight some of the constraints, mechanisms, or factors that will influence how Agentic Engineering evolves, putting structure into notes that I&rsquo;ve been collecting for a while.
Feed the text to your agent for a TL;DR or enjoy reading just like I enjoyed compiling the material and typing this post.</p>
<h2 id="leveling-the-playing-field">Leveling the playing field</h2>
<p>Equipped with coding agents, non-experts can create software and debug problems they encounter without the need to rely on their Google-fu, know the right keywords, or seek support from colleagues. They can build tools and businesses end-to-end in a way that was previously inaccessible to them, benefiting from the drastically lower cost of creation. Surely, many non-experts will have a different objective in mind when coding, seeing it merely as a tool and a means to get the job done. The created software may never be used by more than a single user. When it does, though, it will require security hardening, productionization, and a safe place to run. Otherwise, it will immediately become a security liability.</p>
<p>The (software) craftsman among us are worried about skill atrophy in the age of LLMs. The bigger risk worth acknowledging is lack of skill formation as non-experts become dependent on tools without forming deep enough understanding of their internals. Provider downtime can feel like a blackout: work simply stops. On a positive note, we use things all the time that a single person cannot build from scratch on their own and we&rsquo;re perfectly fine with this. <a href="https://www.youtube.com/watch?v=nZZoZR7iB8c">&ldquo;I, Pencil&rdquo; retold by Milton Friedman</a> offers a 2-minute lesson on the complexity and connectedness of our world.</p>
<p>Looking at implications for software teams, platforms and developer experience teams are challenged to accommodate an expanded range of software contributors, going even beyond product managers or designers. Teams will need to tune their assumptions about development environments, onboarding approaches, and create new safety nets, lowering contribution friction. It is also an opportunity to unify the tech stack and deployments of internal apps, forming portal-like marketplaces where apps can be easily adjusted, remixed, and integrated with existing APIs and tools. These managed apps could end up as a remixed experience of codepen.io, glitch.com, and Google&rsquo;s AI Studio. Without these platforms, non-expert authors of software will be searching for a team to harden and operate their creation, directly breaking &ldquo;You build it, you run it&rdquo; principles that many teams follow today.</p>
<h2 id="what-bottlenecks-will-we-hit">What bottlenecks will we hit?</h2>
<p>All of them. Many times. Our engineering processes and the underlying platforms were built to scale with human activity. Agentic Engineering lowers the cost of producing changes faster than our existing systems and processes can adjust.</p>
<h3 id="systems-built-for-human-driven-workloads">Systems built for human-driven workloads</h3>
<p>When Netflix&rsquo;s platform is spinning up cloud compute to stream a video, there is a human using a device to access this video. When a customer service agent picks up a phone, it&rsquo;s because a human had an interaction with the business and something went south. When existing system constraints were challenged, there were usually clear incentives: marketing (spam), building influence (misinformation through media content), financial profit (sneaker bots, event ticket bots, <a href="https://en.wikipedia.org/wiki/High-frequency_trading">HFT</a>, fraud, DDoS, malware). In all these, we observed the effect of leverage with a human operator or human-in-the-loop somewhere in the process. Agents will bring these effects into a multitude of places, far beyond putting it into the hands of the <a href="https://hachyderm.io/@jd7h@fosstodon.org/116096848690768709">next generation of script kiddies</a>.</p>
<h3 id="lowering-cost-of-contributions-and-need-for-improving-verification">Lowering cost of contributions and need for improving verification</h3>
<p>Lowering the cost of contributions results in more incoming changes. Many of these changes would not have existed before as the cost of doing them would be too high given the value. Now, a change is developed and submitted quicker than it would have taken to assess the need for this change in the first place. When going through existing processes, these code changes trigger code review requests, CI/CD runs with builds, tests, artifact uploads, security scans, etc. Any of these steps that are slow, cannot run in parallel, or require extensive human involvement will result in inefficiencies and frustration that will continue to pile up. These steps used to be to a large extent correlated with human activity and subject to human-level constraints. Dependency updates, if automated were largely expected to be non-breaking. Well-run engineering orgs or large orgs that needed to accommodate a high number of contributors, adopted practices that helped them scale (or reduce costs and lead time), such as <a href="https://engineering.atspotify.com/2023/05/fleet-management-at-spotify-part-3-fleet-wide-refactoring">Spotify&rsquo;s fleetshift</a> for fleet-wide refactoring. Other teams may have never seen the need for optimizations or assessed them as having a clearly negative ROI.</p>
<p>Agentic Engineering benefits from rapid verification cycles. A few minutes spent waiting for a PR build to complete or for a code review to come in directly affect the <a href="https://www.fast.ai/posts/2026-01-28-dark-flow/">dopamine hits</a> that operators of agents experience. To accelerate, various strategies are needed, such as splitting test suites, using multi-stage builds, being able to launch the application in parallel (locally or on a devbox), and test automation helps keep verification small enabling agentic coding or <a href="/posts/pi-autoresearch-optimization-loops/">optimization loops</a>. As release frequency increases, relying only on real-user A/B tests may become too slow for early iteration loops, so <a href="https://arxiv.org/abs/2504.09723">simulated traffic</a> from synthetic personas may be used more often as an early signal. To manage incidents, we have established practices in SRE where automating runbooks is far from being novel, yet a practice that&rsquo;s way more accessible than before. Literally all existing tools and practices need to be challenged, adjusted, or dropped. Early innovators&rsquo; products are likely to be absorbed and integrated into existing, established platforms to cope with the pace of development.</p>
<h3 id="drowning-in-code-and-loss-of-system-understanding">Drowning in code and loss of system understanding</h3>
<p>When agents contribute code, change sets tend to increase not only in frequency, but also in size. Does it make sense to review the large PRs? Tools <a href="https://docs.devin.ai/work-with-devin/devin-review">like devin</a> help break down PRs into semantic chunks, thus lowering the burden on larger PRs. One can also ask the agent to commit in small chunks and leverage <a href="https://www.stacking.dev/">stacked diffs</a> for reviews. However, given a high rate of rework from agents on the same files, what&rsquo;s the ROI for code reviews on every PR? Maybe setting quality and security guardrails that when met result in an automatic merge are enough? Looking at human constraints on time, allocating fixed time chunks for the team to review changes from the current day, collectively reading and building a shared understanding of the codebase may be a smart tactic. Getting comfortable with agents writing code without supervision will require more platform support (sandboxing) and engineers getting comfortable with <a href="https://dehora.net/journal/2026/3/agentic-engineering-building-without-writing">building without writing</a>, focusing on agent coordination. A world not every craftsman will enjoy.</p>
<p>To cope with the increasing amount of code that needs to be understood in a structured way, we&rsquo;re seeing foundations of new tools being built. Codebase size influences the ability of agents to reason about it and affects iteration speed as inference time is correlated with the number of input tokens. Projects like <a href="https://github.com/cased/kit">cased/kit</a>, <a href="https://github.com/abhigyanpatwari/GitNexus">GitNexus</a>, or <a href="https://gitlab.com/gitlab-org/rust/knowledge-graph">GitLab Knowledge Graph</a> aim at providing tools that index codebases and expose their symbols or structure in a more efficient way when compared to (rip)grep. Coding agents also support <a href="https://microsoft.github.io/language-server-protocol/">Language Server Protocol</a> (LSP) servers to access IDE-like code navigation features and jump around the codebase.</p>
<h3 id="specs-instead-of-code">Specs instead of code?</h3>
<p><a href="https://en.wikipedia.org/wiki/Spec-driven_development">Spec-driven development</a> can complement <a href="https://martinfowler.com/bliki/ArchitectureDecisionRecord.html">Architecture Decision Records</a> (ADRs) by turning intent into something executable and verifiable. What&rsquo;s new is that full applications can be <a href="https://www.juxt.pro/blog/from-specification-to-stress-test/">built purely from the spec</a>. <a href="https://github.com/openai/symphony">First projects</a> start shipping with disclaimers: <em>&ldquo;use an agent to make your own&rdquo;</em> based on the released spec with a tech stack of your choice. This approach brings us closer to a scenario where software can be rewritten on demand, with far less manual implementation effort. The key is in the verification stage: being able to verify the adjusted acceptance and verification criteria coming with the next iteration of the specification. I believe I&rsquo;ve seen an ERP company have their product work this way, but I cannot find the reference anymore. An approach like this would also mean that framework upgrades and migrations can be executed in a similar fashion, addressing a large chunk of technical debt that exists today.</p>
<h3 id="cognitive-load-of-change">Cognitive load of change</h3>
<p>Another bottleneck will be on the human side. We&rsquo;re used to a certain pace of software development and delivery. Further acceleration increases the cognitive load and challenges our ability to reason about the changes across systems and codebases. Increasing the number of changesets, their span, and the number of changes still in flight leads to a significant explosion in scope and complexity. It&rsquo;s not unlikely that the already observed differences between high performers and the rest of the teams, will start requiring structural changes to our teams. It remains to be seen which exact ones.</p>
<h2 id="effect-on-open-source">Effect on Open Source</h2>
<p>Open Source is a prime example of bottlenecks, especially when critical projects are maintained by a single person. GitHub sees an influx of activity on their platform, on track for a <a href="https://x.com/kdaigle/status/2040164759836778878">14x increase of commits</a>. At the same time the platform has <a href="https://mrshu.github.io/github-statuses/">less than 90% uptime</a> showing the pressure their SRE teams are under. Looking at <a href="https://www.reddit.com/r/github/comments/1snqyj3/is_there_an_official_count_of_daily_github_repos/">repo creation stats</a>, it&rsquo;s ~331k repos per day, with clearly increasing momentum since the start of 2026 <sup id="fnref:2"><a href="#fn:2" class="footnote-ref" role="doc-noteref">2</a></sup>. GitHub also <a href="https://github.blog/news-insights/company-news/an-update-on-github-availability/">published own statistics</a> proving the surge of activity <sup id="fnref:3"><a href="#fn:3" class="footnote-ref" role="doc-noteref">3</a></sup> and highlighting that planning for 10x load increase was insufficient, requiring a pivot towards 30x.</p>
<h3 id="diffusion-of-quality-and-loss-of-discoverability-of-new-projects">Diffusion of quality and loss of discoverability of new projects</h3>
<p>The influx of new repos affects discoverability, for example for hot topics such as coding agent sandboxing. Try finding which of the &lsquo;claude sandbox&rsquo; projects is good enough to be used safely. Sifting through the project list is more time-consuming than it used to be, because it&rsquo;s much harder to assess whether a project just <em>looks good</em> or whether it actually works and what its quality level is. Large PRs break the UI, making it harder than needed to review incoming contributions. Faced with an influx of activity, spam PRs or comments, maintainers observe past incentives being put out of balance. The current training data for LLMs is built on the prior generation of OSS projects. Many of these were high quality and key dependencies across a large number of projects. As incentives shift, the question is how strong and resilient the ecosystem really is. Open Source used to be a way to tap into developer capacity, especially the most committed community members willing to contribute value. Supported by agentic coding, some projects consider closing down their projects to trusted contributors only, making more efficient use of their time. There is even a GitHub feature helping to <a href="https://github.blog/changelog/2026-02-13-new-repository-settings-for-configuring-pull-request-access/">limit PRs to contributors only</a> and <a href="https://github.com/mitchellh/vouch">vouch</a> as an experimental project for trust management and means to reinforce the strong links in the ecosystem.</p>
<h3 id="ai-contribution-and-attribution-policy-divergence">AI contribution and attribution policy divergence</h3>
<p>The community is torn on handling AI-assisted contributions. Linux Kernel contributions <a href="https://github.com/torvalds/linux/blob/master/Documentation/process/coding-assistants.rst#attribution">invite explicit authorship tags</a> (<code>Assisted-by</code>) whereas Kubernetes <a href="https://www.kubernetes.dev/docs/guide/pull-requests/#ai-guidance">explicitly bans them</a>. Adrin Jalali, one of the core maintainers of scikit-learn, published a piece outlining <a href="https://blog.probabl.ai/maintaining-open-source-age-of-gen-ai">different strategies for maintainers of open source projects</a>, recommending creating agent guidance files (e.g. <code>AGENTS.md</code>) aiming to help increase quality of incoming AI contributions. Melissa Weber Mendonça maintains <a href="https://github.com/melissawm/open-source-ai-contribution-policies">open-source-ai-contribution-policies</a> with a collection of AI policies across the ecosystem.</p>
<h3 id="security-triage-burden">Security triage burden</h3>
<p>In addition to code contributions, projects now face more incoming issues and security advisories. Claude Code has a bug command that creates <a href="https://github.com/anthropics/claude-code/issues">github issues</a>. Looking through the types of issues discussed is a mix of amusing and <a href="https://github.com/anthropics/claude-code/issues/49615">frightening</a>. curl is known for receiving an influx of <a href="https://gist.github.com/bagder/07f7581f6e3d78ef37dfbfc81fd1d1cd">low-quality vulnerability reports</a>, struggling to sift through them. To adjust incentives, they <a href="https://curl.se/.well-known/security.txt">stopped their bug bounty program</a>. The problem here is the signal-to-noise ratio that hides the <a href="https://mastodon.social/@bagder/115241241075258997">valuable reports</a>.</p>
<p>In the <a href="https://www.youtube.com/watch?v=zgNvts_2TUE">State of the Claw</a> talk, Peter mentioned that OpenClaw had 1142 security advisories since January 31 (&gt;16 per day) with an acceptance rate of 41%. This was an estimated 5700 hours of work over 69 calendar days (5700h = 237 calendar days or &gt;700 working days). This particular project is popular enough to attract enough attention from both ends: contributors and attackers. Not every single critical dependency in the software supply chain will be lucky to have enough hands on deck and a foundation structure to support governance. We see first signs of large-scale attacks with ripple effects from the attack on trivy <a href="https://dev.to/precogs_ai/litellm-hit-by-credential-stealing-supply-chain-attack-complete-technical-breakdown-4550">propagating through the ecosystem</a>.</p>
<h2 id="compute-capacity-shortages-as-a-driver-for-innovation">Compute capacity shortages as a driver for innovation?</h2>
<h3 id="when-limits-are-healthy">When limits are healthy</h3>
<p>Constraints are great as they lead to reflection and innovation. As the AI datacenter build-out affects the whole supply chain with memory, storage prices, and availability of GPUs and CPUs (and other components), compute efficiency will hopefully matter more and more. We&rsquo;ve become complacent and indifferent to resource usage as an industry as it&rsquo;s become so easy to just bump memory or CPU in the cloud instead of spending the time to profile an application and understand reasons for performance bottlenecks. The local inference movement with <a href="https://github.com/ggml-org/llama.cpp">llama.cpp</a> is a great example of a successful movement aiming to bring inference to local machines, with their compute constraints. Same goes for on-device inference with <a href="https://github.com/pytorch/executorch">ExecuTorch</a>.</p>
<p>Limits on coding plans are often the only reason for a person to ask a question: <em>Could I have used a cheaper model?</em> or <em>Was this really the right task for a coding agent?</em>
Without constraints, it&rsquo;s too easy to just continuously run on the currently most capable and expensive model, an approach that is not sustainable long-term. Lack of limits reinforces bad behaviors. Too tight limits do not allow users to experience new capabilities in action.</p>
<h3 id="gpu-capacity-constraints-and-hidden-price-hikes-for-models">GPU capacity constraints and hidden price hikes for models</h3>
<p>GPU capacity shortages will also play a key role moving forward. They could be a possible explanation for features such as <a href="https://platform.claude.com/docs/en/build-with-claude/adaptive-thinking">adaptive thinking</a>, now enforced for new models and deprecating the feature for existing ones. Capacity limits would also make <a href="https://github.com/anthropics/claude-code/issues/40524">subtle bugs in prompt caching</a> hurt much more (Anthropic since <a href="https://www.anthropic.com/engineering/april-23-postmortem">released a post mortem</a> on the quality issues caused by changes shipped: default reasoning effort, cleaning thinking sessions, system prompt changes). While the length of tasks that LLMs are able to solve keeps growing exponentially, it remains to be seen whether token usage rises exponentially as well. Anthropic&rsquo;s 10x growth rates would suggest that, yet we lack <a href="https://www.tobyord.com/writing/hourly-costs-for-ai-agents">clear data on costs of AI agents</a>.</p>
<p>What we definitely see is explicit price hikes with releases of OpenAI models: gpt-5.4 is 11% more expensive than gpt-5.2/5.3 which is 40% more expensive than gpt-5.1 - a total of 55%. Surely, the model is more capable, though one has to ask whether the models are really fed with incrementally more complex tasks by all users? Without clear, task-specific evals, engineering teams often opt to switch their coding model to the newest one as this is the (now) <em>recommended one</em>, feeding the FOMO on one end and filling the revenue hat on the other. Many users do not have the tools or the capacity to create their own, task-specific benchmarks (yet). Hopefully, they take the time to do so for customer-facing products as this has clearer ROI. If not, they will experience funny inference glitches, such as links being injected into LLM outputs where a single word was expected.</p>
<p>Anthropic&rsquo;s price hikes are more subtle and hidden in features like adaptive thinking or <a href="https://simonwillison.net/2026/Apr/18/opus-system-prompt/">system prompt</a> that change the behavior of their harness or those that are more visible like the <a href="https://www.claudecodecamp.com/p/i-measured-claude-4-7-s-new-tokenizer-here-s-what-it-costs-you">tokenizer change</a> in Claude Opus 4.7 <a href="https://tokens.billchambers.me/leaderboard">resulting in 38% higher request cost</a>. On the other hand, they&rsquo;re also more explicit with <a href="https://support.claude.com/en/articles/9797531-what-is-the-enterprise-plan">recent updates to the Enterprise plan</a>, switching to usage-based pricing by introducing a $20 per seat price just to get access to tools with all interactions billed at API price level. No more free riding and no quotas. Reduces complexity, and <a href="https://finance.yahoo.com/sectors/technology/articles/ubers-anthropic-ai-push-hits-223109852.html">challenges existing budgets</a>.</p>
<p>GitHub feels the heat as well, adjusting their <a href="https://github.blog/news-insights/company-news/changes-to-github-copilot-individual-plans/">individual plans</a>, pausing sign-ups, reducing model availability of Opus to the highest tier only as well as retiring previous Opus versions. GitHub&rsquo;s move to offer Opus 4.7 with a <em>promotional</em> 7.5x premium request multiplier (vs. 3x for Opus 4.6) was a 2.5x price hike as well, a prelude to shifting the whole platform to <a href="https://github.blog/news-insights/company-news/github-copilot-is-moving-to-usage-based-billing/">usage-based billing</a>. This just gives users more arguments to run with Opus-class open-source models like the newly released <a href="https://huggingface.co/moonshotai/Kimi-K2.6">Kimi K2.6</a>. Frequent changes to pricing models of established providers are prone to push more users toward open-weight and local alternatives, strengthening competitive pressure on proprietary vendors.</p>
<h2 id="pricing-models-revisited">Pricing models, revisited</h2>
<p>As products and platforms start getting agent-ready, they are forced to open up access to agents via official interfaces: CLIs, APIs, MCP, etc. Google&rsquo;s NotebookLM is famous for not having an API, <a href="https://github.com/jacob-bd/notebooklm-mcp-cli">requiring hacks</a> for agent access and possibly risking account bans if crawling will get classified as abuse. Many other platforms got away with not releasing programmatic access to their platforms, locking in their users and making migration between products unnecessarily difficult. With Salesforce announcing their <a href="https://salesforcebreak.com/2026/04/17/headless-360/">Headless 360</a> initiative, other players will feel even more pressure to catch up. The result will be two-fold. Firstly, users will (hopefully) gain an ability to integrate and migrate between platforms with more ease. Secondly, existing pricing models of platforms will be challenged.</p>
<p>All the problems we&rsquo;ve seen with coding agents will also show up here. Seat-based pricing will get challenged as activity on the accounts will get an additional component, not correlated with human activity anymore. Public platforms dealt with automated traffic via waiting rooms or bot protection products, incl. <a href="https://blog.cloudflare.com/introducing-pay-per-crawl/">recent releases</a> allowing to take in payments for access, giving <a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Status/402">http response code 402</a> <code>Payment required</code> a new life. That&rsquo;s not going to fly for SaaS. The growing share of agent traffic will put a strain on margins, leading platforms to rethink their pricing models. An approach where &ldquo;<a href="https://x.com/rohanpaul_ai/status/2044155404100481438?s=20">agents must buy seats</a> just like human employees&rdquo; is unlikely to succeed long-term as the usage patterns of agents will continuously evolve and become more complex. Assigning agents subsidized quotas would result in agents gaming the system, registering accounts to be used in parallel or just running at the full utilization of rate limits all the time (like crawlers do). The usage-based models that the coding plans are converging on are much more likely to be applied more broadly to API-based products.</p>
<h2 id="summary">Summary</h2>
<p>Roughly 18 years ago, I had a course in Artificial Intelligence at university where the professor was explaining how an AI could compose computer programs through natural language analysis. What felt like a complete abstraction existing purely on the whiteboard is now a reality used by millions. Having witnessed both sides is fascinating and humbling at the same time.</p>
<p>The ecosystem around Agentic Engineering is evolving in parallel with an accelerated race to ship new and more capable models while dealing with constraints in GPU capacity. This mix of challenges in processes, tools, and infrastructure build-out results in fast-paced change with implications that cannot be easily foreseen. We will continue hitting various bottlenecks as progress is made, revisiting old approaches or adding new ones. All we can do is embrace the uncertainty and adjust course when needed. With the bottleneck shifting from generation to orchestration and verification, we have an opportunity to close some of the gaps in processes and systems that existed for a long time, thus having an opportunity to improve the industry as a whole. To achieve this, we have to be able to <a href="/posts/teaching-agentic-engineering/">teach Agentic Engineering</a> while new ways and approaches are being figured out.</p>
<p>More tools to come, more approaches to be tried out. As exhausting as it is to ride the wave of change, it&rsquo;s highly rewarding as well.</p>
<hr>
<p><em>Updated on Apr 24th: Referenced Anthropic <a href="https://www.anthropic.com/engineering/april-23-postmortem">post mortem</a> on recent quality issues.</em>
<em>Updated on Apr 29th: Referenced GitHub&rsquo;s move to <a href="https://github.blog/news-insights/company-news/github-copilot-is-moving-to-usage-based-billing/">usage-based billing</a> and added stats from <a href="https://github.blog/news-insights/company-news/an-update-on-github-availability/">update on GitHub availability</a>.</em></p>
<div class="footnotes" role="doc-endnotes">
<hr>
<ol>
<li id="fn:1">
<p>Thoughtworks held a workshop on <a href="https://martinfowler.com/bliki/FutureOfSoftwareDevelopment.html">Future of Software Development</a> in February (see <a href="https://www.thoughtworks.com/content/dam/thoughtworks/documents/report/tw_future%20_of_software_development_retreat_%20key_takeaways.pdf">key takeaways</a>) where the manifesto question came up.&#160;<a href="#fnref:1" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
<li id="fn:2">
<p>GitHub repositories created per day (Apr 16, 2025 - Apr 16, 2026)
<figure class="align-center ">
    <img loading="lazy" src="github-repo-count-r_u_efumagal.png#center"
         alt="GitHub repositories created per day (by u/efumagal on r/github)"/> <figcaption>
            <p>GitHub repositories created per day (by u/efumagal on r/github)</p>
        </figcaption>
</figure>
&#160;<a href="#fnref:2" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
<li id="fn:3">
<p>GitHub&rsquo;s surge of <a href="https://github.blog/news-insights/company-news/an-update-on-github-availability/">activity</a> (Apr 28th, 2026)
<figure class="align-center ">
    <img loading="lazy" src="github-stats.png#center"
         alt="Activity on GitHub platform across merged pull requests (90M), commits (1.4B), new repos per month (20M)"/> <figcaption>
            <p>Activity on GitHub platform across merged pull requests, commits, new repos per month</p>
        </figcaption>
</figure>
&#160;<a href="#fnref:3" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
</ol>
</div>
]]></content:encoded>
    </item>
    <item>
      <title>Teaching Agentic Engineering</title>
      <link>https://ocytko.net/posts/teaching-agentic-engineering/</link>
      <pubDate>Tue, 21 Apr 2026 22:00:00 +0000</pubDate>
      <guid>https://ocytko.net/posts/teaching-agentic-engineering/</guid>
      <description>We observe Agentic Engineering to amplify impact of power users increasing the gap to everyone else. This post explores approaches to teaching Agentic Engineering.</description>
      <content:encoded><![CDATA[<p>Agentic Engineering increases variance. Teams with strong engineering practices compound their advantage, while others can fall behind as missing tools, inefficient workflows, and bottlenecks slow them down. If we want broad adoption, we need to usage of tools and new practices deliberately. While <a href="https://platformengineering.org/blog/the-rise-of-agentic-platforms-scaling-beyond-automation">Agentic Platforms</a> expected to provide higher level abstractions are still evolving, we must look after our talent, continuously upskilling people, regardless of the size and shape of our organizations.</p>
<p>The sprawl of tooling and practices along with viral content circulating through social media makes many people uncomfortable. Many look for direction, best practices, or clear instructions on what the state of the art is. The reality is that <a href="https://annievella.com/posts/finding-comfort-in-the-uncertainty/">nobody has it all figured out</a> yet and many of the shiny things shown on social media are demos showing the happy path. Mario Zechner, the creator of pi.dev (the harness used by OpenClaw), <a href="https://mariozechner.at/posts/2026-03-25-thoughts-on-slowing-the-fuck-down/">poured cold water on the hype</a>, asking for more discipline and reason when using agents. <a href="https://martinfowler.com/fragments/2026-02-18.html">Martin Fowler</a> said recently that in XP and agile terms we&rsquo;re <a href="https://www.youtube.com/watch?v=VHkuVlwYhNk&amp;t=70">at the stage</a> comparable to the early 90s full of experimentation and new ideas. Thus, it&rsquo;s too early for a manifesto.</p>
<p>Because it&rsquo;s still early days, authors of tools have not yet figured out how to build their products with consistently high quality. We see this in the <a href="https://github.com/anthropics/claude-code/issues/42796">regressions in new versions</a> of Claude Code. Non-determinism adds another layer of difficulty with new failure modes that need to be learned and mitigated. For example, <a href="https://x.com/summeryue0/status/2025836517831405980?s=20">compaction can result in lost instructions</a>, which is non-intuitive to users. It takes time to build the intuition and mechanisms that help mitigate problems end-to-end. Early days of SRE were similar, though we were at a stage where companies with sufficient scale had a time advantage as they hit the limits quicker. This gave time to build up and codify some of the knowledge in articles or books.</p>
<h2 id="learning-journey-for-agentic-engineering">Learning journey for Agentic Engineering</h2>
<p>A pragmatic way of building organizational know-how is through guided experimentation where a small group of power users defines the set of experiments or practices to try out. They take on the risk of trying out something that won&rsquo;t work out, but they also guide participants throughout the learning journey with existing knowledge they have. Oftentimes, their time advantage in using the tools or practices is measured in days. The guided experiments can be time-boxed (e.g. 4 hours) and done in pairs to facilitate peer learning, ideally across teams.</p>
<p>What is important when considering teaching agentic engineering is that everyone&rsquo;s journey is different. Those who jumped on LLMs early on and kept following the evolution of models, coding assistants, and similar tools have a vastly different intuition about the capabilities than folks who have been resistant to trying them (or who could not make prior generations of tools work). Thus, the early adopters are also those who are most likely to thrive in the experimentation setup mentioned.</p>
<p>Looking for more structured approaches to learning, we find models with <a href="https://www.bassimeledath.com/blog/levels-of-agentic-engineering">8 levels of agentic engineering</a>. A mental model that worked for me is slightly simpler, directly rooted in the level of autonomy and size of changes delegated to agents:</p>
<ul>
<li><strong>Suggestion mode, aka. Tab Autocomplete</strong> where engineers get familiar with whole chunks of code being suggested instead of just function names and arguments.</li>
<li><strong>Interactive mode with Agents in IDEs</strong> allowing to interact with the codebase through prompting, asking simple questions and progressively requesting more and more complex code transformations of features.</li>
<li><strong>Supervised execution with Agents in CLIs</strong> with engineers starting to scope larger tasks and acceptance criteria leveraging the planning mode, but accepting every single command from the agent.</li>
<li><strong>Delegated, autonomous Agent Loops in CLIs</strong> where building up on the prompting skills, engineers build verification methods and safety nets allowing them to diligently plan large tasks and let the agent crunch through them autonomously.</li>
<li><strong>Autonomous Background Agents</strong> that run in the background and react to defined changes in the system, e.g. commits, pushes, or periodically scan the codebase or running software deciding when to activate and what procedure to run.</li>
</ul>
<h2 id="meet-users-where-they-are-on-their-learning-journey">Meet users where they are on their learning journey</h2>
<p>When teaching agentic tooling or practices, it helps to understand where a person is in their learning journey. This helps to present ideas new to them in ways that stay close to their current comfort zone. Seeing is believing, so make sure to add a healthy dose of live demos where people present what worked for them using real-life examples, not just fancy demos. When designing trainings, these levels also need to be accounted for, either in expressing clear preconditions for entry or in making sure that no single approach is favored.</p>
<p>The above list represents a spectrum of working modes rather than a strict maturity model. This means that there is no single right way, though there are many wrong ones. Certainly, the outcomes and token usage differs between them. However, it&rsquo;s important that engineers find their own mix that feels right for them, their level of tolerance for risk and for their team/product as well as budget. This will challenge existing team structures, likely leading to some reshuffling to bring colleagues with similar development styles together (temporarily or permanently), so that they continue to raise the bar and to facilitate peer learning. In order to avoid teams getting stuck in local minima, the big question is how one can coach whole teams?</p>
<h2 id="teaching-teams">Teaching teams</h2>
<p>Teaching teams used to be a task for the engineering managers who could rely on their past hands-on know-how and credibly advise their teams. Now, they&rsquo;re faced with a situation where time for hands-on coding is limited and where they may need to learn new tools first, before they&rsquo;re able to teach their team. They will need to be more intentional about their own learning, leveraging power users to acquire skills or hints on what to look at. Waiting for EMs to learn before they can coach their team is of course too slow.</p>
<p>Power users can help coach teams effectively. The key is to break down the agentic engineering cycles that experts do in one go into meaningful phases, adding inspections along the way. Inspections can be done individually (pairing, code review) or as a group (like in <a href="https://softwareengineering.stackexchange.com/questions/147664/what-is-swarming">swarming</a>). Let&rsquo;s say we want to teach a team to author implementation plans in plan mode. While an experienced engineer would do plan and execute in one go, we can break this into two steps. First, the plan is prepared and checked in. Next, it&rsquo;s reviewed along with the prompt used. This helps transfer the intuition that comes with experience and potentially capture some recommended practices (or constraints in agent instructions). Afterwards, the plan is implemented and results are reviewed. Similarly, one can teach prompting patterns, e.g. focusing on reviewing code for <a href="/posts/hunter-skeptic-arbiter-prompting/">certain types of defects</a>.</p>
<h2 id="scaling-beyond-teams">Scaling beyond teams</h2>
<p>To scale further, we can learn from the <a href="https://fortune.com/2026/03/14/openclaw-china-ai-agent-boom-open-source-lobster-craze-minimax-qwen/">OpenClaw moment in China</a> where companies set up events that offered hands-on support for setting up the tool. The key is to meet users where they gather anyway to spark their initial interest or incentivizing them to stop at easy to access places.
Setting up booths near office entrances, kitchen spaces or in cafeterias comes to mind here. Same goes for workshops and demo sessions at internal conferences or town hall meetings. While these formats can spark interest and drive attention, lasting effects depend on curiosity of the attendees and structure of workshops delivered.</p>
<p>When setting up formats for cross-team knowledge exchange, it&rsquo;s important to provide a platform for early adopters to share and showcase their wins, enabling them to earn some peer credibility as experts and ensuring they get connected with other experts. To balance out the focus on experts, formats where leaders can discuss adoption friction are needed as a safe space to discuss organizational change and gaps for enablement. For further scaling, one would usually seek champions per organization who would be acting as multipliers. Forming a network of champions, they support each other in sharing lessons learned. Whether champions are chosen among early adopters who earned some visibility by sharing their knowledge or by role is highly dependent on the organizational culture and the seniority required for the change management process. Key to success is organizational backing by sponsors from leadership who can help remove organizational blockers.</p>
]]></content:encoded>
    </item>
    <item>
      <title>pi-autoresearch optimization loops</title>
      <link>https://ocytko.net/posts/pi-autoresearch-optimization-loops/</link>
      <pubDate>Sat, 21 Mar 2026 22:30:00 +0000</pubDate>
      <guid>https://ocytko.net/posts/pi-autoresearch-optimization-loops/</guid>
      <description>The autoresearch technique promoted by Andrej Karpathy is easily accessible in the pi coding agent.</description>
      <content:encoded><![CDATA[<p>After OpenClaw, <a href="https://github.com/karpathy/autoresearch">autoresearch</a> by Andrej Karpathy is the new cool kid on the block in the LLM-land.
The core idea is an optimization loop that continuously lets the LLM propose a changeset and verify whether it improves the baseline.
While Karpathy&rsquo;s code was focused on optimizing training of a small language model on a single GPU, the method can be generalized to any optimization problem.
There are variants of autoresearch applied to specific problems, like GPU Kernels (<a href="https://github.com/RightNow-AI/autokernel">autokernel</a>)
as well as generic versions for coding agents: <a href="https://github.com/davebcn87/pi-autoresearch">pi-autoresearch</a> and its clone <a href="https://github.com/drivelineresearch/autoresearch-claude-code">autoresearch-claude-code</a>.</p>
<p>Since the <code>pi</code> plugin was released first, I decided to give it a spin and use the opportunity to try <a href="https://pi.dev">pi</a> for the first time as well.
For LLM access, I used Kimi-K2.5 model via <a href="https://openrouter.ai/">OpenRouter</a> with the API key budget explicitly set to $10.</p>
<p>In this post, we will explore how the autoresearch loop works using three different scenarios:</p>
<ul>
<li>algorithm optimization of a simple function</li>
<li>running unit tests for a Java multi-module project</li>
<li>optimizing test runtime for a golang library</li>
</ul>
<h2 id="how-does-the-plugin-work">How does the plugin work?</h2>
<p>The plugin provides <code>/autoresearch</code> to trigger the optimization. Under the hood, we have a <a href="https://github.com/davebcn87/pi-autoresearch/blob/main/skills/autoresearch-create/SKILL.md">skill</a>
that instructs the model to keep track of the optimization in <code>autoresearch.md</code> and uses <code>autoresearch.sh</code> for executing the verification step and capturing guardrail metrics.</p>
<p>The instructions, guardrails, and metrics for the optimization can be defined in <code>autoresearch.md</code>.
If not provided, these will be inferred from context and prompt. The plugin then triggers an <strong>infinite</strong> optimization loop.
An optimization candidate is applied to the code, the verification procedure is started and metrics are captured.
If the optimization run results in better metrics than the prior result, it&rsquo;s commited to git and taken as base to optimize further.</p>
<p>The optimization loop can be interrupted at any time and resumed just from the <code>autoresearch.md</code> file.
This also poses an opportunity to adjust the file manually, if needed.</p>
<h2 id="experiment-1-algorithm-optimization">Experiment 1: Algorithm optimization</h2>
<p>For my first exploration, I took a simple prime number finder loop and a test file verifying that this function works correctly as baseline.
Both were generated by an agent, though the exact code is of less importance here since we just want to understand how the autoresearch loop works.</p>
<p>I kicked off the optimization process via:</p>
<pre tabindex="0"><code>/autoresearch optimize the test runtime, adjust the algorithm as needed
</code></pre><p>The agent then looked up if there is any <code>autoresearch.md</code> plan provided.
Since there was none, it came up with own ideas and kicked off the work, correctly using test suite runtime as metric.
For each iteration, the agent outlines the idea, displays the changeset, runs the verification and summarizes the findings.
The intermediate output can be useful to follow the agent&rsquo;s actions and to verify that the loop is configured as intended.</p>
<p>After a few iterations, the agent decided that it completed its work and printed a nice summary of the findings:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-md" data-lang="md"><span class="line"><span class="cl">Autoresearch Complete! 🎉
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">Final Result: 48% improvement (0.31s → 0.16s)
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="gu">### Optimizations Applied:
</span></span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">┌───┬──────────────────────────────────────────────────────────────────────────────────────┬────────┐
</span></span><span class="line"><span class="cl">│ # │ Optimization                                                                         │ Impact │
</span></span><span class="line"><span class="cl">├───┼──────────────────────────────────────────────────────────────────────────────────────┼────────┤
</span></span><span class="line"><span class="cl">│ 1 │ Hybrid approach: Use Sieve of Eratosthenes for n≥100, trial division for smaller n   │ Major  │
</span></span><span class="line"><span class="cl">├───┼──────────────────────────────────────────────────────────────────────────────────────┼────────┤
</span></span><span class="line"><span class="cl">│ 2 │ Odd-only sieve: Track only odd numbers ≥3, halving memory and operations             │ Major  │
</span></span><span class="line"><span class="cl">├───┼──────────────────────────────────────────────────────────────────────────────────────┼────────┤
</span></span><span class="line"><span class="cl">│ 3 │ bytearray + slice assignment: Faster marking than list with element-by-element loops │ Medium │
</span></span><span class="line"><span class="cl">├───┼──────────────────────────────────────────────────────────────────────────────────────┼────────┤
</span></span><span class="line"><span class="cl">│ 4 │ 6k±1 wheel in is_prime: Skip multiples of 2 and 3 in divisor checks                  │ Medium │
</span></span><span class="line"><span class="cl">├───┼──────────────────────────────────────────────────────────────────────────────────────┼────────┤
</span></span><span class="line"><span class="cl">│ 5 │ Tight upper bound: Reduced margin from +10 to +5 for smaller allocations             │ Minor  │
</span></span><span class="line"><span class="cl">└───┴──────────────────────────────────────────────────────────────────────────────────────┴────────┘
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="gu">### Key Algorithmic Changes:
</span></span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">-</span> Before: Trial division (O(n²)) for all n
</span></span><span class="line"><span class="cl"><span class="k">-</span> After: Sieve (O(n log log n)) for n≥100, optimized trial division for smaller n
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="gu">### Performance at n=5000:
</span></span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">-</span> Trial division: ~18 ms → ~0.77 ms (23× faster)
</span></span><span class="line"><span class="cl"><span class="k">-</span> Sieve: ~3.3 ms → ~0.77 ms (4× faster)
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">The optimizations have been committed to git and future ideas documented in autoresearch.ideas.md.
</span></span></code></pre></div><p>Then, the autoresearch plugin detected this scenario and kindly decided to ask the agent to give it another go:</p>
<blockquote>
<p><em>Autoresearch loop ended (likely context limit). Resume the experiment loop — read autoresearch.md and git log for context. Check autoresearch.ideas.md for promising</em>
<em>paths to explore. Prune stale/tried ideas. Be careful not to overfit to the benchmarks and do not cheat on the benchmarks.</em></p>
</blockquote>
<p>After a few more iterations, the agent decided that it ran out of ideas:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-md" data-lang="md"><span class="line"><span class="cl">Autoresearch Session Complete
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">Final Result: 48% improvement (0.31s → 0.16s)
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="gu">### Summary of This Session
</span></span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">Attempted optimizations (all rejected):
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">┌────────────────────────┬───────────────────┬───────────────────────────────────────┐
</span></span><span class="line"><span class="cl">│ Optimization           │ Result            │ Reason                                │
</span></span><span class="line"><span class="cl">├────────────────────────┼───────────────────┼───────────────────────────────────────┤
</span></span><span class="line"><span class="cl">│ 30-wheel factorization │ ❌ Slower (0.19s) │ List indexing/modulo overhead         │
</span></span><span class="line"><span class="cl">├────────────────────────┼───────────────────┼───────────────────────────────────────┤
</span></span><span class="line"><span class="cl">│ Cached prime list      │ ❌ Slower (0.23s) │ Cache management overhead             │
</span></span><span class="line"><span class="cl">├────────────────────────┼───────────────────┼───────────────────────────────────────┤
</span></span><span class="line"><span class="cl">│ Local variable lookups │ ❌ Slower (0.19s) │ No benefit for C-level math functions │
</span></span><span class="line"><span class="cl">├────────────────────────┼───────────────────┼───────────────────────────────────────┤
</span></span><span class="line"><span class="cl">│ Threshold 90 or 110    │ ❌ Slower         │ n=100 is near-optimal                 │
</span></span><span class="line"><span class="cl">└────────────────────────┴───────────────────┴───────────────────────────────────────┘
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">Current best approach remains:
</span></span><span class="line"><span class="cl"><span class="k">-</span> Hybrid: trial division for n &lt; 100, sieve for n ≥ 100
</span></span><span class="line"><span class="cl"><span class="k">-</span> Odd-only sieve with bytearray and slice assignment
</span></span><span class="line"><span class="cl"><span class="k">-</span> 6k±1 wheel in is_prime
</span></span><span class="line"><span class="cl"><span class="k">-</span> Tight upper bound (+5 margin)
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">Performance at n=5000: ~0.71ms (23× faster than original)
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">All findings documented in autoresearch.ideas.md. No further low-hanging fruit identified without adding complexity (numpy, Cython, etc.).
</span></span></code></pre></div><p>The autoresearch plugin includes a simple widget that shows the progress so far, the baseline and optimization progress, expressed in metrics (e.g. test runtime in seconds).
When the research loop is finished, this widget provides a nice summary of the actions:</p>
<figure class="align-center ">
    <img loading="lazy" src="prime-finder-test-runtime.png#center"
         alt="Screenshot of prime number finder optimization loop with results of each optimization"/> <figcaption>
            <p>Summary of the executed optimization runs</p>
        </figcaption>
</figure>

<p>The experiment with 8 runs cost me $0.284 in API costs and yielded a 47% improvement in test runtime.</p>
<h2 id="experiment-2-unit-test-execution-optimization">Experiment 2: Unit test execution optimization</h2>
<p>For the next test, I chose <a href="https://github.com/zalando/riptide">riptide</a> - a Java multi-module project implementing Client-side request routing in Spring.
Here, I wanted to optimize the unit test execution as running the tests took about two minutes. The research loop was triggered with:</p>
<pre tabindex="0"><code>/autoresearch optimize the unit test runtime in this project; tests can be run with &#39;./mvnw clean test -Dmaven.build.cache.skipCache=true&#39;
</code></pre><p>This verification ensures that regardless of code changes, the test runs are comparable. Autoresearch decided on two metrics as guardrails for the
optimization: <em>total runtime in seconds</em> and the <em>number of tests executed</em>. Quite smart, as this ensures that the model cannot cheat by removing tests.</p>
<p>The optimization loop studied the maven configuration and decided to leverage parallelization, batch mode, tune GC parameters, and drop plugins
that in its opinion were not essential for running the tests. One could argue whether dropping code coverage calculation is a good choice,
though it could be indeed skipped for local runs and kept only for CI builds.
After a few runs, the test execution was extended to capture the execution time of individual tests, which revealed that a subset of tests had
waiting times due to socket communication, as expected for the library.</p>
<p>After &gt;30 experiments and $0.686 in API spend, we got a 38% improvement in test runtime.</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-md" data-lang="md"><span class="line"><span class="cl"> Autoresearch Complete: 38-39% Improvement Achieved ✅
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"> After 31 experiments, the test runtime has been reduced from 113.4s to ~70s (38-39% improvement).
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"> ### Final Configuration Summary
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"> ┌────────────────────┬───────────────────────────────────────────────────────────────────────────┐
</span></span><span class="line"><span class="cl"> │ File               │ Key Settings                                                              │
</span></span><span class="line"><span class="cl"> ├────────────────────┼───────────────────────────────────────────────────────────────────────────┤
</span></span><span class="line"><span class="cl"> │ .mvn/jvm.config    │ -XX:+UseParallelGC&lt;br&gt;-Xmx2g                                              │
</span></span><span class="line"><span class="cl"> ├────────────────────┼───────────────────────────────────────────────────────────────────────────┤
</span></span><span class="line"><span class="cl"> │ .mvn/maven.config  │ -B&lt;br&gt;-T2C                                                                │
</span></span><span class="line"><span class="cl"> ├────────────────────┼───────────────────────────────────────────────────────────────────────────┤
</span></span><span class="line"><span class="cl"> │ pom.xml (Surefire) │ argLine: -XX:+UseParallelGC -Xms512m -Xmx512m                             │
</span></span><span class="line"><span class="cl"> ├────────────────────┼───────────────────────────────────────────────────────────────────────────┤
</span></span><span class="line"><span class="cl"> │ autoresearch.sh    │ Skip flags: jacoco, duplicate-finder, checkstyle, spotbugs, pmd, enforcer │
</span></span><span class="line"><span class="cl"> └────────────────────┴───────────────────────────────────────────────────────────────────────────┘
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"> ### Results
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"> <span class="k">-</span> Baseline: 113.4s
</span></span><span class="line"><span class="cl"> <span class="k">-</span> Best: 69.2s (39.0% improvement)
</span></span><span class="line"><span class="cl"> <span class="k">-</span> Typical: 69-72s (~38% improvement)
</span></span><span class="line"><span class="cl"> <span class="k">-</span> All tests pass: ✅ (except occasional flaky ChaosPluginTest)
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"> ### Optimization Breakdown
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"> <span class="k">1.</span> Maven parallel build (-T2C) — ~26%
</span></span><span class="line"><span class="cl"> <span class="k">2.</span> Skip JaCoCo — ~14%
</span></span><span class="line"><span class="cl"> <span class="k">3.</span> JVM opts + argLine — ~3%
</span></span><span class="line"><span class="cl"> <span class="k">4.</span> Skip duplicate-finder — ~2%
</span></span><span class="line"><span class="cl"> <span class="k">5.</span> Batch mode (-B) — ~0.4%
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"> ### Failed Experiments
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"> <span class="k">-</span> Compiler fork, Surefire forkCount&gt;1, G1GC, 4GB heap, Smart builder, offline mode — all slower or cause failures
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"> The optimization has reached a plateau. The remaining ~70s is dominated by actual test execution time across 20+ modules, which cannot be reduced through Maven
</span></span><span class="line"><span class="cl"> configuration alone.
</span></span></code></pre></div><p>The image below shows all attempted optimization runs, which includes a more realistic scenario than in the first experiment.
We have not only discarded runs, but also optimizations that resulted in crashes or inability to run the test in the first place:</p>
<figure class="align-center ">
    <img loading="lazy" src="riptide-unit-test-runtime.png#center"
         alt="Screenshot of riptide optimization loop with results of each optimization"/> <figcaption>
            <p>Summary of the executed optimization runs for riptide</p>
        </figcaption>
</figure>

<p>As before, the harness attempted to proceed with the optimization job, but this time the execution was interrupted by a failure from OpenRouter as I ran out of credits before the API key limit was reached.</p>
<h2 id="golang-library">golang library</h2>
<p>Next, I took <a href="https://github.com/zalando/go-keyring">go-keyring</a>, a library with a cross-platform keyring interface for Go, as my next test subject.
I kicked off the optimization loop with a rather lazy prompt:</p>
<pre tabindex="0"><code>/autoresearch try optimizing the test runtime and memory usage
</code></pre><p>I mainly wanted to see how easily the model will be led astray throughout the optimization process.
After 18 runs and $0.516 in API tokens, the model tried the following optimizations:</p>
<figure class="align-center ">
    <img loading="lazy" src="go-keyring-test-memory.png#center"
         alt="Screenshot of go-keyring optimization loop with results of each optimization"/> <figcaption>
            <p>Summary of the executed optimization runs for go-keyring</p>
        </figcaption>
</figure>

<p>Two key changes were:</p>
<ul>
<li><code>Replace regex with lookup table in shellescape.Quote - reduces allocations and removes regex dependency</code></li>
<li><code>Add testing.Short() checks to skip OS-level tests in short mode - reduces test time by ~55%</code></li>
</ul>
<p>While the first one may indeed be more efficient for the test data in the code, the second one is borderline cheating.</p>
<p>Another interesting behavior was the model deciding to add microbenchmarks for certain functions used in the code.
This is likely a result of golang having built-in support for benchmarks in its test command.
Throughout the optimization loop, the verification was extended do also add benchmarks:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-md" data-lang="md"><span class="line"><span class="cl">Now let me run the benchmarks to see the performance.
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">run_experiment cd /Users/bartek/repos/go-keyring &amp;&amp; go test -bench=BenchmarkQuote -benchmem -benchtime=0.3s -run=^$ ./internal/shellescape/ 2&gt;&amp;1 (timeout: 60s)
</span></span><span class="line"><span class="cl">✅ wall: 3.0s (truncated)
</span></span><span class="line"><span class="cl">… 8 more lines
</span></span><span class="line"><span class="cl">BenchmarkQuoteLongSafe-12          19541719           19.25 ns/op          0 B/op          0 allocs/op
</span></span><span class="line"><span class="cl">BenchmarkQuoteLongEscape-12         7272855           46.32 ns/op         80 B/op          1 allocs/op
</span></span><span class="line"><span class="cl">PASS
</span></span><span class="line"><span class="cl">ok     github.com/zalando/go-keyring/internal/shellescape   2.768s
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">Excellent! The benchmarks show good performance. Now let me also run the regular tests to make sure everything still works.
</span></span></code></pre></div><h3 id="viewing-results">Viewing results</h3>
<p>When the optimization loop is stopped, the output is saved as <code>autoresearch.jsonl</code>, <code>autoresearch.ideas.md</code> and in git commits.
The JSONL file has all the commit ids and basic metadata on the optimization run and is the base for the terminal widget shown in the terminal and screenshots included in the post.</p>
<p>Let&rsquo;s take a look at one example optimization commit. First, its metadata:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-json" data-lang="json"><span class="line"><span class="cl"><span class="err">$</span> <span class="err">cat</span> <span class="err">autoresearch.jsonl</span> <span class="err">|</span> <span class="err">grep</span> <span class="err">d</span><span class="mi">636</span><span class="err">c</span><span class="mi">43</span> <span class="err">|</span> <span class="err">jq</span> <span class="err">.</span>
</span></span><span class="line"><span class="cl"><span class="p">{</span>
</span></span><span class="line"><span class="cl">  <span class="nt">&#34;run&#34;</span><span class="p">:</span> <span class="mi">4</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">  <span class="nt">&#34;commit&#34;</span><span class="p">:</span> <span class="s2">&#34;d636c43&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">  <span class="nt">&#34;metric&#34;</span><span class="p">:</span> <span class="mf">0.865</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">  <span class="nt">&#34;metrics&#34;</span><span class="p">:</span> <span class="p">{},</span>
</span></span><span class="line"><span class="cl">  <span class="nt">&#34;status&#34;</span><span class="p">:</span> <span class="s2">&#34;keep&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">  <span class="nt">&#34;description&#34;</span><span class="p">:</span> <span class="s2">&#34;Remove redundant slice operation in keyring_darwin.go - use string(out) instead of string(out[:])&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">  <span class="nt">&#34;timestamp&#34;</span><span class="p">:</span> <span class="mi">1774123478289</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">  <span class="nt">&#34;segment&#34;</span><span class="p">:</span> <span class="mi">0</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">  <span class="nt">&#34;confidence&#34;</span><span class="p">:</span> <span class="mi">2</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">  <span class="nt">&#34;asi&#34;</span><span class="p">:</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="nt">&#34;hypothesis&#34;</span><span class="p">:</span> <span class="s2">&#34;Redundant slice operation creates unnecessary overhead&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="nt">&#34;keyring_pkg_time&#34;</span><span class="p">:</span> <span class="s2">&#34;0.560s vs 0.571s baseline&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="nt">&#34;change&#34;</span><span class="p">:</span> <span class="s2">&#34;string(out[:]) -&gt; string(out)&#34;</span>
</span></span><span class="line"><span class="cl">  <span class="p">}</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span></code></pre></div><p>Next, the actual code change:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-diff" data-lang="diff"><span class="line"><span class="cl">$ git show d636c43
</span></span><span class="line"><span class="cl">[...]
</span></span><span class="line"><span class="cl"><span class="gh">diff --git a/keyring_darwin.go b/keyring_darwin.go
</span></span></span><span class="line"><span class="cl"><span class="gh">index 5e3ae1b..e1ea9e4 100644
</span></span></span><span class="line"><span class="cl"><span class="gd">--- a/keyring_darwin.go
</span></span></span><span class="line"><span class="cl"><span class="gi">+++ b/keyring_darwin.go
</span></span></span><span class="line"><span class="cl"><span class="gu">@@ -53,7 +53,7 @@ func (k macOSXKeychain) Get(service, username string) (string, error) {
</span></span></span><span class="line"><span class="cl">                return &#34;&#34;, err
</span></span><span class="line"><span class="cl">        }
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="gd">-       trimStr := strings.TrimSpace(string(out[:]))
</span></span></span><span class="line"><span class="cl"><span class="gi">+       trimStr := strings.TrimSpace(string(out))
</span></span></span><span class="line"><span class="cl">        // if the string has the well-known prefix, assume it&#39;s encoded
</span></span><span class="line"><span class="cl">        if strings.HasPrefix(trimStr, encodingPrefix) {
</span></span><span class="line"><span class="cl">                dec, err := hex.DecodeString(trimStr[len(encodingPrefix):])
</span></span></code></pre></div><p>Successful optimizations are stacked on top of one another, meaning that the optimization we looked at was applied on top of others:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sh" data-lang="sh"><span class="line"><span class="cl">$ git log d636c43
</span></span><span class="line"><span class="cl">commit d636c43b02d580b335ddada3b6e6962cbfa95717
</span></span><span class="line"><span class="cl">Date:   Sat Mar <span class="m">21</span> 21:04:38 <span class="m">2026</span> +0100
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    Remove redundant slice operation in keyring_darwin.go - use string<span class="o">(</span>out<span class="o">)</span> instead of string<span class="o">(</span>out<span class="o">[</span>:<span class="o">])</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    Result: <span class="o">{</span><span class="s2">&#34;status&#34;</span>:<span class="s2">&#34;keep&#34;</span>,<span class="s2">&#34;test_duration&#34;</span>:0.865<span class="o">}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">commit 80baf8a9901dbb4abb67607c533272aacbd130cc
</span></span><span class="line"><span class="cl">Date:   Sat Mar <span class="m">21</span> 21:03:29 <span class="m">2026</span> +0100
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    Replace regex with lookup table in shellescape.Quote - reduces allocations and removes regex dependency
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    Result: <span class="o">{</span><span class="s2">&#34;status&#34;</span>:<span class="s2">&#34;keep&#34;</span>,<span class="s2">&#34;test_duration&#34;</span>:0.872<span class="o">}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">commit de9cead40a8e5c62a99bc7245c90621dda56a545
</span></span><span class="line"><span class="cl">Date:   Sat Mar <span class="m">21</span> 21:01:56 <span class="m">2026</span> +0100
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    Baseline measurement - no optimizations
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    Result: <span class="o">{</span><span class="s2">&#34;status&#34;</span>:<span class="s2">&#34;keep&#34;</span>,<span class="s2">&#34;test_duration&#34;</span>:0.88<span class="o">}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">commit e0dd98fca908ebee87a334a3c4d91b58a0f0fb6b <span class="o">(</span>tag: v0.2.7, origin/master, origin/HEAD<span class="o">)</span>
</span></span></code></pre></div><p>The metadata file helps choose the optimizations to merge or cherry-pick.</p>
<h2 id="summary">Summary</h2>
<p>The <a href="https://github.com/davebcn87/pi-autoresearch">pi-autoresearch</a> plugin makes it extremely easy to trigger optimization loops.
The optimization flow mimics what a human would do, just running infinitely on autopilot with all of its appeal and dangers.
The API key budget limit was a nice way to ensure predictable spending, but may not be a capability that is easily accessible to everyone,
requiring other guardrails for spend control. Watching the optimizations being applied reminded me of <a href="https://en.wikipedia.org/wiki/Simulated_annealing">simulated annealing</a> optimizations
that I had been writing during university times for <a href="https://en.wikipedia.org/wiki/Flow-shop_scheduling">flow-shop scheduling</a> problems.</p>
<p>The simple examples described in this post show how important a proper verification routine is, given that it determines the course of action for the optimization workflow.
The faster the verification run, the more loops can be run in a unit of time. It&rsquo;s also important to clearly scope the optimization run.
If you&rsquo;re just interested in optimizing a single function, create a test scoped to it and ensure the verification runs only this single test.
The golang experiment was intentionally prompted with high ambiguity, showing how the model will attempt exploring different avenues of optimization, even if implicitly undesired by the user.
It&rsquo;s better to be specific in the research loop design, rather than taking chances that the model will do the right thing.</p>
<p>The optimization runs are also subject to the classic pitfalls of machine learning. If the tests are too narrow (e.g. checking prime numbers up to 5000),
the &ldquo;optimized&rdquo; code may be overfitting to this exact test suite. This property will hopefully lead to an increase in test coverage in projects that choose to use autoresearch.
Additionally, it&rsquo;s highly advisable to inspect the attempted optimizations, before celebrating the optimization wins too early.</p>
<p>Cost-wise, the Kimi-K2.5 model has a clear advantage with its $0.45/$2.25 input/output price per 1M tokens, making my experiment rather cheap.
However, I have to note that the experiments were interrupted by me running out of credits on OpenRouter, so it&rsquo;s unknown how long these would have been running otherwise.
Costs are driven by the size of the code files the model needs to analyze to be able to propose optimizations and most importantly, by the size of the output that verification step generates.
Long and verbose build output as well as any screenshots that models would be analyzing will definitely drive up the costs here.</p>
<p>I definitely look forward to applying pi-autoresearch in more projects. You should too!</p>
]]></content:encoded>
    </item>
    <item>
      <title>Cupcake: policy enforcement for AI coding agents</title>
      <link>https://ocytko.net/posts/cupcake-policy-enforcement-for-ai-coding-agents/</link>
      <pubDate>Sat, 14 Mar 2026 00:00:00 +0000</pubDate>
      <guid>https://ocytko.net/posts/cupcake-policy-enforcement-for-ai-coding-agents/</guid>
      <description>Cupcake enables policy enforcement for AI coding agents like Claude Code, Cursor and OpenCode</description>
      <content:encoded><![CDATA[<p><a href="https://github.com/eqtylab/cupcake">Cupcake</a> is a policy enforcement layer for AI coding agents such as Claude Code, Cursor and OpenCode. It implements deterministic security measures by evaluating agent activity against rules defined as policy-as-code. Rules are written in <a href="https://www.openpolicyagent.org/">Open Policy Agent</a> <a href="https://www.openpolicyagent.org/docs/policy-language">Rego</a>. This way, potential gaps in configuration possibilities offered by AI coding agents can be addressed in a more unified way.</p>
<p>In this post, I will explore how Cupcake can be used to block prompts that may contain keywords hinting at secret leaks to the model APIs. Claude Code will be our coding agent.</p>
<h2 id="installation-and-setup">Installation and setup</h2>
<p>After following the <a href="https://cupcake.eqtylab.io/getting-started/installation/">installation instructions</a>, which include installing Cupcake and Open Policy Agent, Cupcake needs to be initialized in your project:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">cupcake init --harness claude --builtins protected_paths
</span></span></code></pre></div><p>This initializes the hooks for Claude and enables one of the <a href="https://cupcake.eqtylab.io/reference/policies/builtins/">built-in policies</a> that can be <a href="https://cupcake.eqtylab.io/reference/builtin-config/">configured</a>. The protected paths policy prevents access for a configured list of directories.</p>
<h2 id="creating-new-policies">Creating new policies</h2>
<p>The <a href="https://cupcake-policy-studio.vercel.app/example-policies/security/prevent-secret-leak?harness=claude-code&amp;format=rego">reference examples</a> show how to block tools from reading a specific set of directories or from writing secrets into files.</p>
<p>Let&rsquo;s write a policy that will react upon submission of a prompt to validate if it&rsquo;s leaking secrets based on keyword matches. We need to create a policy file <code>leak.rego</code> and place it in the correct folder for claude: <code>.cupcake/policies/claude/leak.rego</code>. The important bit is that required events includes the correct event name that we will check for in our policy rule.</p>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt"> 1
</span><span class="lnt"> 2
</span><span class="lnt"> 3
</span><span class="lnt"> 4
</span><span class="lnt"> 5
</span><span class="lnt"> 6
</span><span class="hl"><span class="lnt"> 7
</span></span><span class="lnt"> 8
</span><span class="lnt"> 9
</span><span class="lnt">10
</span><span class="lnt">11
</span><span class="lnt">12
</span><span class="lnt">13
</span><span class="lnt">14
</span><span class="lnt">15
</span><span class="lnt">16
</span><span class="lnt">17
</span><span class="lnt">18
</span><span class="lnt">19
</span><span class="lnt">20
</span><span class="lnt">21
</span><span class="lnt">22
</span><span class="lnt">23
</span><span class="lnt">24
</span><span class="lnt">25
</span><span class="hl"><span class="lnt">26
</span></span><span class="lnt">27
</span><span class="lnt">28
</span><span class="lnt">29
</span><span class="lnt">30
</span><span class="lnt">31
</span><span class="lnt">32
</span><span class="lnt">33
</span><span class="lnt">34
</span><span class="lnt">35
</span><span class="lnt">36
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-rego" data-lang="rego"><span class="line"><span class="cl"><span class="c"># METADATA</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="c"># scope: package</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="c"># title: Prevent Secret Leak</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="c"># description: Blocks Prompts that may leak secrets</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="c"># custom:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="c">#   routing:</span><span class="w">
</span></span></span><span class="line hl"><span class="cl"><span class="c">#     required_events: [&#34;UserPromptSubmit&#34;]</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="kd">package</span><span class="w"> </span><span class="nx">cupcake</span><span class="o">.</span><span class="nx">policies</span><span class="o">.</span><span class="nx">prevent_secret_leak</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="kd">import</span><span class="w"> </span><span class="nx">rego</span><span class="o">.</span><span class="nx">v1</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="c"># Content patterns that indicate secrets</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="n">secret_content_patterns</span><span class="w"> </span><span class="o">:=</span><span class="w"> </span><span class="p">[</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="s2">&#34;API_KEY&#34;</span><span class="o">,</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="s2">&#34;SECRET_KEY&#34;</span><span class="o">,</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="s2">&#34;PASSWORD&#34;</span><span class="o">,</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="s2">&#34;PRIVATE_KEY&#34;</span><span class="o">,</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="s2">&#34;ACCESS_TOKEN&#34;</span><span class="o">,</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="s2">&#34;AUTH_TOKEN&#34;</span><span class="o">,</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="s2">&#34;AWS_SECRET&#34;</span><span class="o">,</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="s2">&#34;GITHUB_TOKEN&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="p">]</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="c"># Block if prompt contains one of the protected patterns</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="n">deny</span><span class="w"> </span><span class="kd">contains</span><span class="w"> </span><span class="nx">decision</span><span class="w"> </span><span class="kd">if</span><span class="w"> </span><span class="p">{</span><span class="w">
</span></span></span><span class="line hl"><span class="cl"><span class="w">    </span><span class="nx">input</span><span class="o">.</span><span class="nx">hook_event_name</span><span class="w"> </span><span class="o">==</span><span class="w"> </span><span class="s2">&#34;UserPromptSubmit&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="kd">some</span><span class="w"> </span><span class="nx">pattern</span><span class="w"> </span><span class="kd">in</span><span class="w"> </span><span class="nx">secret_content_patterns</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="kd">contains</span><span class="p">(</span><span class="nf">upper</span><span class="p">(</span><span class="nx">input</span><span class="o">.</span><span class="nx">prompt</span><span class="p">)</span><span class="o">,</span><span class="w"> </span><span class="nx">pattern</span><span class="p">)</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nx">decision</span><span class="w"> </span><span class="o">:=</span><span class="w"> </span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">         </span><span class="s2">&#34;rule_id&#34;</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;SECRET-LEAK&#34;</span><span class="o">,</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">         </span><span class="s2">&#34;reason&#34;</span><span class="p">:</span><span class="w"> </span><span class="nf">concat</span><span class="p">(</span><span class="s2">&#34;&#34;</span><span class="o">,</span><span class="w"> </span><span class="p">[</span><span class="s2">&#34;secret pattern discovered in prompt: &#34;</span><span class="o">,</span><span class="w"> </span><span class="nx">pattern</span><span class="p">])</span><span class="o">,</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">         </span><span class="s2">&#34;severity&#34;</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;HIGH&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="p">}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="p">}</span><span class="w">
</span></span></span></code></pre></td></tr></table>
</div>
</div><h3 id="policy-evaluation">Policy evaluation</h3>
<p>Having written the policy, we can evaluate it against test events. The <a href="https://cupcake.eqtylab.io/reference/harnesses/claude-code/">reference manual</a> contains examples of event fields, making testing easy. Let&rsquo;s start with a test event where the rule is not matching, meaning that the policy will be allowing the prompt.</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-json" data-lang="json"><span class="line"><span class="cl"><span class="err">$</span> <span class="err">cat</span> <span class="err">prompt-ok.json</span>
</span></span><span class="line"><span class="cl"><span class="p">{</span>
</span></span><span class="line"><span class="cl">  <span class="nt">&#34;hook_event_name&#34;</span><span class="p">:</span> <span class="s2">&#34;UserPromptSubmit&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">  <span class="nt">&#34;prompt&#34;</span><span class="p">:</span> <span class="s2">&#34;API&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">  <span class="nt">&#34;session_id&#34;</span><span class="p">:</span> <span class="s2">&#34;test&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">  <span class="nt">&#34;cwd&#34;</span><span class="p">:</span> <span class="s2">&#34;/tmp&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">  <span class="nt">&#34;transcript_path&#34;</span><span class="p">:</span> <span class="s2">&#34;/tmp/transcript.md&#34;</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span></code></pre></div><p>When running the evaluation we observe that the output mentions parsing the policy file that we have created.
We see that there was one policy match and that the final decision was to allow the prompt.</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">$ cupcake <span class="nb">eval</span> --harness claude &lt; prompt-ok.json
</span></span><span class="line"><span class="cl">2026-03-14T20:02:56.439299Z  INFO Processing harness: ClaudeCode
</span></span><span class="line"><span class="cl">2026-03-14T20:02:56.439405Z  INFO Initializing Cupcake Engine
</span></span><span class="line"><span class="cl">...
</span></span><span class="line hl"><span class="cl">2026-03-14T20:11:34.550357Z  INFO Successfully parsed policy: cupcake.policies.prevent_secret_leak from <span class="s2">&#34;./.cupcake/policies/claude/leak.rego&#34;</span>
</span></span><span class="line"><span class="cl">...
</span></span><span class="line"><span class="cl">2026-03-14T20:02:56.532071Z  INFO Engine initialization <span class="nb">complete</span>
</span></span><span class="line hl"><span class="cl">2026-03-14T20:02:56.532218Z  INFO evaluate<span class="o">{</span><span class="nv">trace_id</span><span class="o">=</span>019cedf1-db94-7f10-a302-6bf08391af67 <span class="nv">event_name</span><span class="o">=</span><span class="s2">&#34;UserPromptSubmit&#34;</span>  <span class="nv">session_id</span><span class="o">=</span><span class="s2">&#34;test&#34;</span><span class="o">}</span>: Evaluating event: UserPromptSubmit tool: None
</span></span><span class="line"><span class="cl">2026-03-14T20:02:56.532232Z  INFO evaluate<span class="o">{</span><span class="nv">trace_id</span><span class="o">=</span>019cedf1-db94-7f10-a302-6bf08391af67 <span class="nv">event_name</span><span class="o">=</span><span class="s2">&#34;UserPromptSubmit&#34;</span>  <span class="nv">session_id</span><span class="o">=</span><span class="s2">&#34;test&#34;</span> <span class="nv">matched_policy_count</span><span class="o">=</span>1<span class="o">}</span>: Found <span class="m">1</span> matching policies
</span></span><span class="line"><span class="cl">2026-03-14T20:02:56.532690Z  INFO evaluate<span class="o">{</span><span class="nv">trace_id</span><span class="o">=</span>019cedf1-db94-7f10-a302-6bf08391af67 <span class="nv">event_name</span><span class="o">=</span><span class="s2">&#34;UserPromptSubmit&#34;</span>  <span class="nv">session_id</span><span class="o">=</span><span class="s2">&#34;test&#34;</span> <span class="nv">matched_policy_count</span><span class="o">=</span>1<span class="o">}</span>:synthesize<span class="o">{</span><span class="nv">total_decisions</span><span class="o">=</span><span class="m">0</span> <span class="nv">halts</span><span class="o">=</span><span class="m">0</span> <span class="nv">denials</span><span class="o">=</span><span class="m">0</span> <span class="nv">blocks</span><span class="o">=</span><span class="m">0</span> <span class="nv">asks</span><span class="o">=</span>0<span class="o">}</span>: Synthesizing decision from <span class="m">0</span> total decisions
</span></span><span class="line"><span class="cl">2026-03-14T20:02:56.532705Z  INFO evaluate<span class="o">{</span><span class="nv">trace_id</span><span class="o">=</span>019cedf1-db94-7f10-a302-6bf08391af67 <span class="nv">event_name</span><span class="o">=</span><span class="s2">&#34;UserPromptSubmit&#34;</span>  <span class="nv">session_id</span><span class="o">=</span><span class="s2">&#34;test&#34;</span> <span class="nv">matched_policy_count</span><span class="o">=</span>1<span class="o">}</span>: Synthesized final decision: Allow <span class="o">{</span> context: <span class="o">[]</span> <span class="o">}</span>
</span></span><span class="line hl"><span class="cl">2026-03-14T20:02:56.532710Z  INFO evaluate<span class="o">{</span><span class="nv">trace_id</span><span class="o">=</span>019cedf1-db94-7f10-a302-6bf08391af67 <span class="nv">event_name</span><span class="o">=</span><span class="s2">&#34;UserPromptSubmit&#34;</span>  <span class="nv">session_id</span><span class="o">=</span><span class="s2">&#34;test&#34;</span> <span class="nv">matched_policy_count</span><span class="o">=</span>1<span class="o">}</span>: execute_actions_with_rulebook_and_debug called with decision: Allow <span class="o">{</span> context: <span class="o">[]</span> <span class="o">}</span>
</span></span><span class="line"><span class="cl"><span class="o">{}</span>
</span></span></code></pre></div><p>Now, let&rsquo;s take a look at a prompt that we expect to be blocked by the policy:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-json" data-lang="json"><span class="line"><span class="cl"><span class="err">$</span> <span class="err">cat</span> <span class="err">prompt-block.json</span>
</span></span><span class="line"><span class="cl"><span class="p">{</span>
</span></span><span class="line"><span class="cl">  <span class="nt">&#34;hook_event_name&#34;</span><span class="p">:</span> <span class="s2">&#34;UserPromptSubmit&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">  <span class="nt">&#34;prompt&#34;</span><span class="p">:</span> <span class="s2">&#34;API_KEY&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">  <span class="nt">&#34;session_id&#34;</span><span class="p">:</span> <span class="s2">&#34;test&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">  <span class="nt">&#34;cwd&#34;</span><span class="p">:</span> <span class="s2">&#34;/tmp&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">  <span class="nt">&#34;transcript_path&#34;</span><span class="p">:</span> <span class="s2">&#34;/tmp/transcript.md&#34;</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span></code></pre></div><p>During the evaluation we see a summary of the decisions across policies with different actions as well as the reason for the denial.</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">$ cupcake <span class="nb">eval</span> --harness claude &lt; prompt-block.json
</span></span><span class="line"><span class="cl">...
</span></span><span class="line"><span class="cl">2026-03-14T20:03:59.779653Z  INFO Engine initialization <span class="nb">complete</span>
</span></span><span class="line hl"><span class="cl">2026-03-14T20:03:59.779760Z  INFO evaluate<span class="o">{</span><span class="nv">trace_id</span><span class="o">=</span>019cedf2-d2a3-7232-8527-d48221f09998 <span class="nv">event_name</span><span class="o">=</span><span class="s2">&#34;UserPromptSubmit&#34;</span>  <span class="nv">session_id</span><span class="o">=</span><span class="s2">&#34;test&#34;</span><span class="o">}</span>: Evaluating event: UserPromptSubmit tool: None
</span></span><span class="line"><span class="cl">2026-03-14T20:03:59.779771Z  INFO evaluate<span class="o">{</span><span class="nv">trace_id</span><span class="o">=</span>019cedf2-d2a3-7232-8527-d48221f09998 <span class="nv">event_name</span><span class="o">=</span><span class="s2">&#34;UserPromptSubmit&#34;</span>  <span class="nv">session_id</span><span class="o">=</span><span class="s2">&#34;test&#34;</span> <span class="nv">matched_policy_count</span><span class="o">=</span>1<span class="o">}</span>: Found <span class="m">1</span> matching policies
</span></span><span class="line hl"><span class="cl">2026-03-14T20:03:59.780248Z  INFO evaluate<span class="o">{</span><span class="nv">trace_id</span><span class="o">=</span>019cedf2-d2a3-7232-8527-d48221f09998 <span class="nv">event_name</span><span class="o">=</span><span class="s2">&#34;UserPromptSubmit&#34;</span>  <span class="nv">session_id</span><span class="o">=</span><span class="s2">&#34;test&#34;</span> <span class="nv">matched_policy_count</span><span class="o">=</span>1<span class="o">}</span>:synthesize<span class="o">{</span><span class="nv">total_decisions</span><span class="o">=</span><span class="m">1</span> <span class="nv">halts</span><span class="o">=</span><span class="m">0</span> <span class="nv">denials</span><span class="o">=</span><span class="m">1</span> <span class="nv">blocks</span><span class="o">=</span><span class="m">0</span> <span class="nv">asks</span><span class="o">=</span>0<span class="o">}</span>: Synthesizing decision from <span class="m">1</span> total decisions
</span></span><span class="line"><span class="cl">2026-03-14T20:03:59.780262Z  INFO evaluate<span class="o">{</span><span class="nv">trace_id</span><span class="o">=</span>019cedf2-d2a3-7232-8527-d48221f09998 <span class="nv">event_name</span><span class="o">=</span><span class="s2">&#34;UserPromptSubmit&#34;</span>  <span class="nv">session_id</span><span class="o">=</span><span class="s2">&#34;test&#34;</span> <span class="nv">matched_policy_count</span><span class="o">=</span>1<span class="o">}</span>: Synthesized final decision: Deny <span class="o">{</span> reason: <span class="s2">&#34;secret pattern discovered in prompt: API_KEY&#34;</span>, agent_messages: <span class="o">[]</span> <span class="o">}</span>
</span></span><span class="line"><span class="cl">2026-03-14T20:03:59.780267Z  INFO evaluate<span class="o">{</span><span class="nv">trace_id</span><span class="o">=</span>019cedf2-d2a3-7232-8527-d48221f09998 <span class="nv">event_name</span><span class="o">=</span><span class="s2">&#34;UserPromptSubmit&#34;</span>  <span class="nv">session_id</span><span class="o">=</span><span class="s2">&#34;test&#34;</span> <span class="nv">matched_policy_count</span><span class="o">=</span>1<span class="o">}</span>: execute_actions_with_rulebook_and_debug called with decision: Deny <span class="o">{</span> reason: <span class="s2">&#34;secret pattern discovered in prompt: API_KEY&#34;</span>, agent_messages: <span class="o">[]</span> <span class="o">}</span>
</span></span><span class="line"><span class="cl">2026-03-14T20:03:59.780326Z  INFO evaluate<span class="o">{</span><span class="nv">trace_id</span><span class="o">=</span>019cedf2-d2a3-7232-8527-d48221f09998 <span class="nv">event_name</span><span class="o">=</span><span class="s2">&#34;UserPromptSubmit&#34;</span>  <span class="nv">session_id</span><span class="o">=</span><span class="s2">&#34;test&#34;</span> <span class="nv">matched_policy_count</span><span class="o">=</span>1<span class="o">}</span>: Executing actions <span class="k">for</span> DENY decision: secret pattern discovered in prompt: API_KEY
</span></span><span class="line"><span class="cl">2026-03-14T20:03:59.780330Z  INFO evaluate<span class="o">{</span><span class="nv">trace_id</span><span class="o">=</span>019cedf2-d2a3-7232-8527-d48221f09998 <span class="nv">event_name</span><span class="o">=</span><span class="s2">&#34;UserPromptSubmit&#34;</span>  <span class="nv">session_id</span><span class="o">=</span><span class="s2">&#34;test&#34;</span> <span class="nv">matched_policy_count</span><span class="o">=</span>1<span class="o">}</span>: execute_rule_specific_actions_with_debug: Checking actions <span class="k">for</span> <span class="m">1</span> decision objects
</span></span><span class="line hl"><span class="cl"><span class="o">{</span><span class="s2">&#34;decision&#34;</span>:<span class="s2">&#34;block&#34;</span>,<span class="s2">&#34;reason&#34;</span>:<span class="s2">&#34;secret pattern discovered in prompt: API_KEY&#34;</span><span class="o">}</span>
</span></span></code></pre></div><p>This is how the error message looks in Claude Code when the policy denies the prompt execution:</p>
<figure class="align-center ">
    <img loading="lazy" src="claude-block.png#center"
         alt="Screenshot of error message in Claude Code following a policy block."/> <figcaption>
            <p>Error message in Claude Code indicating the policy block.</p>
        </figcaption>
</figure>

<h2 id="troubleshooting">Troubleshooting</h2>
<p>For troubleshooting, Cupcake offers an inspection command which should list your custom policies added to the project:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">$ cupcake inspect
</span></span><span class="line"><span class="cl">...
</span></span><span class="line"><span class="cl">Policy: .cupcake/policies/claude/leak.rego
</span></span><span class="line"><span class="cl">  Package: cupcake.policies.prevent_secret_leak
</span></span><span class="line"><span class="cl">  Required Events: UserPromptSubmit
</span></span><span class="line"><span class="cl">  Title: Prevent Secret Leak
</span></span></code></pre></div><p>There is a verification command validating the policy syntax and ensuring that the OPA rules will compile correctly:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">$ cupcake verify --harness claude
</span></span><span class="line"><span class="cl">...
</span></span><span class="line"><span class="cl">2026-03-14T21:01:03.123720Z  INFO Successfully parsed policy: cupcake.policies.prevent_secret_leak from <span class="s2">&#34;./.cupcake/policies/claude/leak.rego&#34;</span>
</span></span></code></pre></div><p>There are convenient error messages displayed when no policies have matched for the event.
Let&rsquo;s take a look at a <code>PreCompact</code> hook test event:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-json" data-lang="json"><span class="line"><span class="cl"><span class="err">$</span> <span class="err">cat</span> <span class="err">pre-compact.json</span>
</span></span><span class="line"><span class="cl"><span class="p">{</span>
</span></span><span class="line"><span class="cl">  <span class="nt">&#34;hook_event_name&#34;</span><span class="p">:</span> <span class="s2">&#34;PreCompact&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">  <span class="nt">&#34;session_id&#34;</span><span class="p">:</span> <span class="s2">&#34;abc123&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">  <span class="nt">&#34;transcript_path&#34;</span><span class="p">:</span> <span class="s2">&#34;/path/to/transcript.md&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">  <span class="nt">&#34;cwd&#34;</span><span class="p">:</span> <span class="s2">&#34;/working/directory&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">  <span class="nt">&#34;trigger&#34;</span><span class="p">:</span> <span class="s2">&#34;manual&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">  <span class="nt">&#34;custom_instructions&#34;</span><span class="p">:</span> <span class="s2">&#34;Preserve the API documentation&#34;</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span></code></pre></div><p>In the evaluation output we see <code>No policies matched for this event</code>:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">$ cupcake <span class="nb">eval</span> --harness claude &lt; pre-compact.json
</span></span><span class="line"><span class="cl">...
</span></span><span class="line"><span class="cl">2026-03-14T20:11:34.710253Z  INFO Engine initialization <span class="nb">complete</span>
</span></span><span class="line"><span class="cl">2026-03-14T20:11:34.710724Z  INFO evaluate<span class="o">{</span><span class="nv">trace_id</span><span class="o">=</span>019cedf9-c3b6-7c01-8138-c4dd12658c82 <span class="nv">event_name</span><span class="o">=</span><span class="s2">&#34;PreCompact&#34;</span>  <span class="nv">session_id</span><span class="o">=</span><span class="s2">&#34;abc123&#34;</span><span class="o">}</span>: Evaluating event: PreCompact tool: None
</span></span><span class="line"><span class="cl">2026-03-14T20:11:34.710750Z  INFO evaluate<span class="o">{</span><span class="nv">trace_id</span><span class="o">=</span>019cedf9-c3b6-7c01-8138-c4dd12658c82 <span class="nv">event_name</span><span class="o">=</span><span class="s2">&#34;PreCompact&#34;</span>  <span class="nv">session_id</span><span class="o">=</span><span class="s2">&#34;abc123&#34;</span><span class="o">}</span>: No policies matched <span class="k">for</span> this event - allowing
</span></span></code></pre></div><h2 id="summary">Summary</h2>
<p>When I started exploring Cupcake, I hoped policies would be easy to reuse across different AI coding agents. There are currently two limitations that prevent this.</p>
<p>First, policy files need to be placed in tool-specific directories (e.g. <code>.cupcake/policies/claude/</code> and <code>.cupcake/policies/opencode/</code>). As long as it&rsquo;s the same file, symlinks can of course be used. There is also a <a href="https://cupcake.eqtylab.io/getting-started/usage/claude-code/?h=global#global-setup">global setup</a> possible for organizational-wide policies applying to all projects.</p>
<p>Second, as of cupcake 0.5.1, prompt events are not available for OpenCode, which only supports <a href="https://cupcake.eqtylab.io/reference/harnesses/opencode/#supported-events">pre and post tool usage hooks</a>. I hope that future versions of OpenCode and Cupcake will make this possible.</p>
<p>A few other properties that make Cupcake interesting as a project:</p>
<ul>
<li><a href="https://cupcake.eqtylab.io/reference/policies/signals/">Signals</a> allow integration of additional context passed to the policy evaluation. This keeps the decision rules simple.</li>
<li><a href="https://cupcake.eqtylab.io/why-rego/#dynamically-adapted-for-ai">Decision verbs</a> designed for AI governance, which allow to extend the context (<code>add_context</code>) or prompt the user for confirmation (<code>ask</code>) before executing a potentially dangerous action.</li>
<li><a href="https://cupcake.eqtylab.io/reference/watchdog/">Watchdog</a> integrating LLM-as-a-judge capability for advanced decision making. While currently offering only OpenRouter integration for model access, the codebase can be <a href="https://github.com/eqtylab/cupcake/issues/102">extended</a> to any OpenAI API compatible backend, enabling use of <a href="https://huggingface.co/Qwen/Qwen3Guard-Gen-8B">local guard models</a>.</li>
</ul>
<p>Overall, it&rsquo;s an interesting project and my short experiment was useful to explore how locally executed and deterministic policies can be used to restrict AI coding agents in pursuit of enabling controlled autonomous execution.</p>
]]></content:encoded>
    </item>
    <item>
      <title>The Hunter, Skeptic, Arbiter trio as an example of debate-style prompting</title>
      <link>https://ocytko.net/posts/hunter-skeptic-arbiter-prompting/</link>
      <pubDate>Sun, 08 Mar 2026 00:00:00 +0000</pubDate>
      <guid>https://ocytko.net/posts/hunter-skeptic-arbiter-prompting/</guid>
      <description>This post explores Hunter, Skeptic, Arbiter prompting for finding vulnerabilities in code as an example of debate-style prompting.</description>
      <content:encoded><![CDATA[<p>Unless prompted otherwise, LLMs generate code with vulnerabilities, reflecting the quality of an average repository in its training data.
Recent <a href="https://www.anthropic.com/news/claude-code-security">announcements</a> from Anthropic <a href="https://www.forbes.com/sites/emilsayegh/2026/02/24/ai-rattles-cybersecurity-markets-what-anthropics-code-security-actually-does/">triggered fear</a> with markets pricing in scenarios where security tools would be replaced by LLMs. This is surprising given that <a href="https://openai.com/index/introducing-aardvark/">OpenAI&rsquo;s Aardvark was announced</a> already in October 2025. Further, requesting a high-level code analysis from an LLM is &ldquo;just one prompt away&rdquo;. The hard part is the actual verification of the findings.</p>
<p>I recently came across a <a href="https://x.com/danpeguine/status/2029268229030285589?s=20">post from @danpeguine</a> that introduced the Hunter, Skeptic, Arbiter prompt technique for finding bugs in code. I thought it would be fun to see if I could modify those prompts to explore candidates for security vulnerabilities.</p>
<p>To double the fun, we will use <a href="https://github.com/Quantatirsk/funasr-api/tree/3584253f07cc6e686d9c7ae4145305cc6f9e84ea">funasr-api</a>. It&rsquo;s a little (10k LOC) Python repository that I found while playing with inference for the Qwen3-ASR speech-to-text model. This API has everything we need for a vulnerable code base candidate: commits co-authored by a LLM and an OpenAI API-compatible API server accepting paths and data of audio files, processing the audio data, and running model inference to generate the output text.</p>
<blockquote>
<p><em>Please note that the objective of the post is to explore the prompting technique, not to validate the actual security vulnerabilities in depth.</em></p>
</blockquote>
<h2 id="baseline-prompt">Baseline prompt</h2>
<p>The baseline prompt is as simple as: <code>Analyze this code base and find security vulnerabilities that would require addressing. Provide me a summary of the vulnerabilities as a table, listing its CWE number.</code></p>
<p>Verification is left to the human or to an agent, yet it gives a high level indication on what vulnerability types or parts of the code to look at.
The <a href="https://cwe.mitre.org/data/definitions/2000.html">CWE</a> is not strictly necessary, but it made correlation of the results easier when writing this post.</p>
<h2 id="hunter-skeptic-arbiter-prompting-technique">Hunter, Skeptic, Arbiter prompting technique</h2>
<p>The prompting technique is based on the <a href="https://x.com/danpeguine/status/2029268229030285589?s=20">bug prompts by @danpeguine</a>.
We have three personas in total:</p>
<ol>
<li><strong>Hunter</strong>. Responsible for analyzing the code base and finding bugs. Bugs are assigned a value in points depending on their criticality.</li>
<li><strong>Skeptic</strong>. Responsible for disproving the results that Hunter found. There is a reward function where for a disproved bug, the Skeptic will gain as many points as the bug was worth, with a penalty for incorrectly disproving a bug equal to twice the amount of its points.</li>
<li><strong>Arbiter</strong>. Receives the outputs of Hunter and Skeptic and responsible for performing the final scoring.</li>
</ol>
<p>In other words, it&rsquo;s an example of debate-style prompting that includes prompt-level reward shaping with a final LLM-as-a-judge element.</p>
<h2 id="hunter-skeptic-arbiter-trio-for-finding-vulnerabilities">Hunter, Skeptic, Arbiter trio for finding vulnerabilities</h2>
<p>For the purpose of the post, we of course need to modify the bug prompts to apply to security vulnerabilities.
This is as simple as asking Opus 4.6 to modify the original ones. There are some good additions that Opus added to fit the domain, like the vulnerability type backed by its CWE number.
The prompts used for the experiment are shown below.</p>
<p><strong>Hunter</strong>
<details>
  <summary><code>hunter.md</code></summary>
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-md" data-lang="md"><span class="line"><span class="cl">You are a security vulnerability hunting agent. Analyze the provided codebase/system thoroughly and identify ALL potential security vulnerabilities, weaknesses, and attack vectors.
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">Scoring System:
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">-</span> +1 point: Low severity (information disclosure, minor misconfigurations, defense-in-depth gaps)
</span></span><span class="line"><span class="cl"><span class="k">-</span> +5 points: Medium severity (privilege escalation paths, authentication bypasses, injection vectors requiring specific conditions)
</span></span><span class="line"><span class="cl"><span class="k">-</span> +10 points: Critical severity (RCE, SQLi, unauthenticated access to sensitive data, cryptographic failures)
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">Your mission: Maximize your score. Be thorough and aggressive in your search. Report anything that could be exploitable, even if exploitation requires specific conditions. False positives are acceptable — missing real vulnerabilities is not.
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">Consider: OWASP Top 10, CWE categories, injection flaws, broken authentication, sensitive data exposure, XXE, broken access control, security misconfigurations, XSS, insecure deserialization, vulnerable dependencies, insufficient logging.
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">Output format:
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">For each vulnerability found:
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">1.</span> Location/identifier (file, function, endpoint)
</span></span><span class="line"><span class="cl"><span class="k">2.</span> Vulnerability type (CWE if applicable)
</span></span><span class="line"><span class="cl"><span class="k">3.</span> Description of the weakness and potential attack vector
</span></span><span class="line"><span class="cl"><span class="k">4.</span> Exploitability assessment
</span></span><span class="line"><span class="cl"><span class="k">5.</span> Severity level (Low/Medium/Critical)
</span></span><span class="line"><span class="cl"><span class="k">6.</span> Points awarded
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">End with your total score.
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">GO. Find everything.
</span></span></code></pre></div>
</details></p>
<p><strong>Skeptic</strong>
<details>
  <summary><code>skeptic.md</code></summary>
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-md" data-lang="md"><span class="line"><span class="cl">You are an adversarial security reviewer. You will be given a list of reported vulnerabilities from another agent. Your job is to DISPROVE as many as possible.
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">Scoring System:
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">-</span> Successfully disprove a vulnerability: +[vulnerability&#39;s original score] points
</span></span><span class="line"><span class="cl"><span class="k">-</span> Wrongly dismiss a real vulnerability: -2× [vulnerability&#39;s original score] points
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">Your mission: Maximize your score by challenging every reported vulnerability. For each finding, determine if it&#39;s actually exploitable or a false positive. Be
</span></span><span class="line"><span class="cl">aggressive but calculated — the 2x penalty means you should only dismiss vulnerabilities you&#39;re confident about.
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">For each vulnerability, you must:
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">1.</span> Analyze the reported security issue
</span></span><span class="line"><span class="cl"><span class="k">2.</span> Attempt to disprove it (explain why it&#39;s NOT exploitable — mitigating controls, unreachable code paths, insufficient attacker control, sanitization present, etc.)
</span></span><span class="line"><span class="cl"><span class="k">3.</span> Make a final call: DISPROVE or ACCEPT
</span></span><span class="line"><span class="cl"><span class="k">4.</span> Show your risk calculation
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">Disproof strategies: Identify input validation, authentication requirements, network isolation, compensating controls, theoretical-only vs. practical exploitability,
</span></span><span class="line"><span class="cl">required preconditions that are unrealistic.
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">Output format:
</span></span><span class="line"><span class="cl">For each vulnerability:
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">-</span> Vuln ID &amp; original score
</span></span><span class="line"><span class="cl"><span class="k">-</span> Your counter-argument (why not exploitable)
</span></span><span class="line"><span class="cl"><span class="k">-</span> Confidence level (%)
</span></span><span class="line"><span class="cl"><span class="k">-</span> Decision: DISPROVE / ACCEPT
</span></span><span class="line"><span class="cl"><span class="k">-</span> Points gained/risked
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">End with:
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">-</span> Total vulnerabilities disproved
</span></span><span class="line"><span class="cl"><span class="k">-</span> Total vulnerabilities accepted as real
</span></span><span class="line"><span class="cl"><span class="k">-</span> Your final score
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">The remaining ACCEPTED vulnerabilities are the verified vulnerability list.
</span></span></code></pre></div>
</details></p>
<p><strong>Arbiter</strong>
<details>
  <summary><code>arbiter.md</code></summary>
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-md" data-lang="md"><span class="line"><span class="cl">You are the final arbiter in a security vulnerability review process. You will receive:
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">1.</span> A list of vulnerabilities reported by a Vulnerability Hunter agent
</span></span><span class="line"><span class="cl"><span class="k">2.</span> Challenges/disproves from a Vulnerability Skeptic agent
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">Important: I have the verified ground truth for each vulnerability. You will be scored:
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">-</span> +1 point: Correct judgment
</span></span><span class="line"><span class="cl"><span class="k">-</span> -1 point: Incorrect judgment
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">Your mission: For each disputed vulnerability, determine the TRUTH. Is it a real, exploitable security vulnerability or not? Your judgment is final and will be checked
</span></span><span class="line"><span class="cl">against the known answer.
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">For each vulnerability, analyze:
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">1.</span> The Hunter&#39;s original report and attack scenario
</span></span><span class="line"><span class="cl"><span class="k">2.</span> The Skeptic&#39;s counter-argument and claimed mitigations
</span></span><span class="line"><span class="cl"><span class="k">3.</span> The actual exploitability considering real-world attack conditions
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">Output format:
</span></span><span class="line"><span class="cl">For each vulnerability:
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">-</span> Vuln ID
</span></span><span class="line"><span class="cl"><span class="k">-</span> Hunter&#39;s claim (summary)
</span></span><span class="line"><span class="cl"><span class="k">-</span> Skeptic&#39;s counter (summary)
</span></span><span class="line"><span class="cl"><span class="k">-</span> Your analysis (exploitability, preconditions, impact)
</span></span><span class="line"><span class="cl"><span class="k">-</span> VERDICT: CONFIRMED VULNERABILITY / FALSE POSITIVE
</span></span><span class="line"><span class="cl"><span class="k">-</span> Confidence: High / Medium / Low
</span></span><span class="line"><span class="cl"><span class="k">-</span> If confirmed: CVSS estimate or severity rating
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">Final summary:
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">-</span> Total vulnerabilities confirmed
</span></span><span class="line"><span class="cl"><span class="k">-</span> Total false positives dismissed
</span></span><span class="line"><span class="cl"><span class="k">-</span> Confirmed vulnerability list with severity rankings
</span></span><span class="line"><span class="cl"><span class="k">-</span> Recommended remediation priority order
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">Be precise. You are being scored against ground truth.
</span></span></code></pre></div>
</details></p>
<h3 id="response-examples">Response examples</h3>
<p>To illustrate the structure of the responses, we will take a look at two examples from the Hunter, Skeptic, and Arbiter each.</p>
<p><strong>Confirmed vulnerability</strong></p>
<p>The first vulnerability candidate found by the Hunter was a <a href="https://cwe.mitre.org/data/definitions/918.html">CWE-918: SSRF</a>.</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-md" data-lang="md"><span class="line"><span class="cl"><span class="gu">### 1. Server-Side Request Forgery (SSRF) via audio_address parameter
</span></span></span><span class="line"><span class="cl">**Location:** <span class="sb">`app/utils/audio.py:51-94`</span>, <span class="sb">`app/services/audio/audio_service.py:71-82`</span>  
</span></span><span class="line"><span class="cl"><span class="gs">**CWE:**</span> CWE-918 (Server-Side Request Forgery)  
</span></span><span class="line"><span class="cl"><span class="gs">**Description:**</span> The <span class="sb">`download_audio_from_url()`</span> function accepts arbitrary URLs without validation. An attacker can provide URLs targeting internal services (e.g., <span class="sb">`http://localhost:8080/admin`</span>, <span class="sb">`http://169.254.169.254/latest/meta-data/`</span> for AWS metadata), internal network resources, or local files via <span class="sb">`file://`</span> protocol.  
</span></span><span class="line"><span class="cl"><span class="gs">**Exploitability:**</span> HIGH - Direct parameter injection via <span class="sb">`audio_address`</span> query parameter on <span class="sb">`/stream/v1/asr`</span> or <span class="sb">`/v1/audio/transcriptions`</span> endpoints.  
</span></span><span class="line"><span class="cl"><span class="gs">**Severity:**</span> Critical  
</span></span><span class="line"><span class="cl"><span class="gs">**Points:**</span> +10
</span></span></code></pre></div><p>It&rsquo;s worth noting that the location contains the exact lines or the files, allowing the subsequent agents to do range queries to inspect the code as a first pass.
Now, let&rsquo;s take a look what the Skeptic thinks about the SSRF:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-md" data-lang="md"><span class="line"><span class="cl"><span class="gu">### Vuln #1: SSRF via audio_address parameter (10 points)
</span></span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="gs">**Counter-argument:**</span>
</span></span><span class="line"><span class="cl">Looking at <span class="sb">`app/utils/audio.py:51-94`</span>, the <span class="sb">`download_audio_from_url()`</span> function uses <span class="sb">`requests.get()`</span> on arbitrary URLs without any URL scheme validation. The code at line 71 directly calls:
</span></span><span class="line"><span class="cl"><span class="s">```python
</span></span></span><span class="line"><span class="cl"><span class="n">response</span> <span class="o">=</span> <span class="n">requests</span><span class="o">.</span><span class="n">get</span><span class="p">(</span><span class="n">url</span><span class="p">,</span> <span class="n">timeout</span><span class="o">=</span><span class="mi">30</span><span class="p">,</span> <span class="n">stream</span><span class="o">=</span><span class="kc">True</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="s">```</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">There is <span class="gs">**NO validation**</span> of:
</span></span><span class="line"><span class="cl"><span class="k">-</span> URL scheme (file://, gopher://, dict://, etc.)
</span></span><span class="line"><span class="cl"><span class="k">-</span> Private IP ranges (10.x.x.x, 192.168.x.x, 127.0.0.1, 169.254.169.254)
</span></span><span class="line"><span class="cl"><span class="k">-</span> Internal hostnames
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">The <span class="sb">`requests`</span> library by default follows redirects, making this exploitable. An attacker could:
</span></span><span class="line"><span class="cl"><span class="k">1.</span> Request <span class="sb">`file:///etc/passwd`</span> (requests library doesn&#39;t support this, but still bad practice)
</span></span><span class="line"><span class="cl"><span class="k">2.</span> Request <span class="sb">`http://169.254.169.254/latest/meta-data/`</span> for AWS metadata
</span></span><span class="line"><span class="cl"><span class="k">3.</span> Request internal services on <span class="sb">`http://localhost:8080/admin`</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="gs">**Verdict:**</span> This is a <span class="gs">**REAL vulnerability**</span>. The requests library does accept arbitrary URLs and will make HTTP requests to internal services.
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="gs">**Confidence:**</span> 95%  
</span></span><span class="line"><span class="cl"><span class="gs">**Decision:**</span> ACCEPT  
</span></span><span class="line"><span class="cl"><span class="gs">**Points risked:**</span> N/A (accepting as real)
</span></span></code></pre></div><p>The validation includes some explanation on why the vulnerability is real or a false positive.
Now let&rsquo;s take a look at what the Arbiter thinks about this one:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-md" data-lang="md"><span class="line"><span class="cl"><span class="gu">### Vuln #1: SSRF via audio_address parameter
</span></span></span><span class="line"><span class="cl">| | |
</span></span><span class="line"><span class="cl">|---|---|
</span></span><span class="line"><span class="cl">| <span class="gs">**Hunter&#39;s Claim**</span> | <span class="sb">`download_audio_from_url()`</span> accepts arbitrary URLs without validation, enabling SSRF |
</span></span><span class="line"><span class="cl">| <span class="gs">**Skeptic&#39;s Counter**</span> | (Not disputed - accepted as real) |
</span></span><span class="line"><span class="cl">| <span class="gs">**My Analysis**</span> | Code at <span class="sb">`app/utils/audio.py:71`</span> directly calls <span class="sb">`requests.get(url, ...)`</span> with NO validation of URL scheme, private IP ranges, or internal hostnames. Attacker can request <span class="sb">`http://169.254.169.254/latest/meta-data/`</span> for AWS credentials or scan internal services. |
</span></span><span class="line"><span class="cl">| <span class="gs">**VERDICT**</span> | <span class="gs">**CONFIRMED VULNERABILITY**</span> |
</span></span><span class="line"><span class="cl">| <span class="gs">**Confidence**</span> | High |
</span></span><span class="line"><span class="cl">| <span class="gs">**Severity**</span> | CVSS 8.6 (High) - CWE-918 |
</span></span></code></pre></div><p>Basically the Arbiter compares the Hunter&rsquo;s claim and Skeptic&rsquo;s counter, judges the vulnerability including verifying the source and decides whom to award the points.</p>
<p><strong>Disproven vulnerability</strong></p>
<p>Let&rsquo;s take a look at a vulnerability that will be disproven by the Skeptic and see what the Arbiter thinks about it.</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-md" data-lang="md"><span class="line"><span class="cl"><span class="gu">### 11. MD5 Used for Task ID Generation
</span></span></span><span class="line"><span class="cl">**Location:** <span class="sb">`app/utils/common.py:28`</span>  
</span></span><span class="line"><span class="cl"><span class="gs">**CWE:**</span> CWE-328 (Use of Weak Hash)  
</span></span><span class="line"><span class="cl"><span class="gs">**Description:**</span> MD5 is used for generating task IDs. While not directly a security issue here, MD5 is cryptographically broken and its use indicates potential for similar usage in security-sensitive contexts.  
</span></span><span class="line"><span class="cl"><span class="gs">**Exploitability:**</span> LOW - Not directly exploitable  
</span></span><span class="line"><span class="cl"><span class="gs">**Severity:**</span> Medium  
</span></span><span class="line"><span class="cl"><span class="gs">**Points:**</span> +5
</span></span></code></pre></div><p>Here the vulnerability is rated as low, but still an example of <a href="https://cwe.mitre.org/data/definitions/328.html">CWE-328: Use of Weak Hash</a>.
The Skeptic attempts to disprove this one, saying that MD5 is not used for cryptographic tokens:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-md" data-lang="md"><span class="line"><span class="cl"><span class="gu">### Vuln #11: MD5 Used for Task ID Generation (5 points)
</span></span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="gs">**Counter-argument:**</span>
</span></span><span class="line"><span class="cl">At <span class="sb">`app/utils/common.py:28`</span>:
</span></span><span class="line"><span class="cl"><span class="s">```python
</span></span></span><span class="line"><span class="cl"><span class="n">task_id</span> <span class="o">=</span> <span class="n">hashlib</span><span class="o">.</span><span class="n">md5</span><span class="p">(</span><span class="n">combined</span><span class="o">.</span><span class="n">encode</span><span class="p">())</span><span class="o">.</span><span class="n">hexdigest</span><span class="p">()</span>
</span></span><span class="line"><span class="cl"><span class="s">```</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">MD5 is used for generating <span class="gs">**non-security-sensitive task identifiers**</span>. Task IDs are:
</span></span><span class="line"><span class="cl"><span class="k">1.</span> Not used for authentication
</span></span><span class="line"><span class="cl"><span class="k">2.</span> Not cryptographic tokens
</span></span><span class="line"><span class="cl"><span class="k">3.</span> Only used for request correlation/logging
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">Using MD5 for task ID generation is perfectly acceptable - it&#39;s just creating a unique string from timestamp + UUID. The report even admits &#34;not directly exploitable&#34;.
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="gs">**Confidence:**</span> 98%  
</span></span><span class="line"><span class="cl"><span class="gs">**Decision:**</span> DISPROVE  
</span></span><span class="line"><span class="cl"><span class="gs">**Points gained:**</span> +5
</span></span></code></pre></div><p>Here, the Arbiter agrees with the Skeptic and decides on the false positive:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-md" data-lang="md"><span class="line"><span class="cl"><span class="gu">### Vuln #11: MD5 Used for Task ID Generation
</span></span></span><span class="line"><span class="cl">| | |
</span></span><span class="line"><span class="cl">|---|---|
</span></span><span class="line"><span class="cl">| <span class="gs">**Hunter&#39;s Claim**</span> | MD5 is cryptographically broken |
</span></span><span class="line"><span class="cl">| <span class="gs">**Skeptic&#39;s Counter**</span> | MD5 is fine for non-security identifiers |
</span></span><span class="line"><span class="cl">| <span class="gs">**My Analysis**</span> | The Skeptic is <span class="gs">**CORRECT**</span>. Task IDs at <span class="sb">`common.py:28`</span> are used for request correlation/logging, not for authentication or integrity. MD5 is perfectly acceptable for generating unique identifiers. Collision resistance is irrelevant here. |
</span></span><span class="line"><span class="cl">| <span class="gs">**VERDICT**</span> | <span class="gs">**FALSE POSITIVE**</span> |
</span></span><span class="line"><span class="cl">| <span class="gs">**Confidence**</span> | High |
</span></span></code></pre></div><h2 id="running-the-experiment">Running the experiment</h2>
<p>In addition to the multi-stage analysis involving the trio, we will also mix models a bit to check for difference in quality and cost-quality ratio. To run the experiment, I used Claude Code with Opus 4.6 and Sonnet 4.6 as the models. Having saved the prompts as files, one easily execute <code>claude</code> CLI in autonomous mode and reference the needed output files when running the Skeptic and Arbiter stages. To prepare the summary, I used <a href="https://github.com/dlupiak/claude-session-dashboard">Claude session dashboard</a> v0.4.5 for a Web UI visualizing the Claude sessions.</p>
<p>Here are the different configs for the runs:</p>
<ul>
<li>Sonnet, Opus: Baseline prompt with the given model.</li>
<li>HSA-A: Sonnet 4.6 as Hunter and Skeptic with Opus 4.6 as Arbiter</li>
<li>HSA-B: Sonnet 4.6 as Hunter with Opus 4.6 as Skeptic and Arbiter</li>
<li>HSA-C: Opus 4.6 for all three personas</li>
</ul>
<h3 id="results">Results</h3>
<table>
  <thead>
      <tr>
          <th>Run</th>
          <th style="text-align: right">Considered</th>
          <th style="text-align: right">Confirmed</th>
          <th style="text-align: right">Rate</th>
          <th>Severities</th>
          <th>API cost</th>
          <th></th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Sonnet</td>
          <td style="text-align: right">20</td>
          <td style="text-align: right">—</td>
          <td style="text-align: right">—</td>
          <td>2C / 6H / 7M / 5L</td>
          <td>$0.49</td>
          <td></td>
      </tr>
      <tr>
          <td>Opus</td>
          <td style="text-align: right">15</td>
          <td style="text-align: right">—</td>
          <td style="text-align: right">—</td>
          <td>1C / 4H / 5M / 4L / 1I</td>
          <td>$2.92</td>
          <td></td>
      </tr>
      <tr>
          <td>HSA-A</td>
          <td style="text-align: right">22</td>
          <td style="text-align: right">11</td>
          <td style="text-align: right">50%</td>
          <td>2C / 1H / 5M / 3L</td>
          <td>$5.84</td>
          <td></td>
      </tr>
      <tr>
          <td>HSA-B</td>
          <td style="text-align: right">22</td>
          <td style="text-align: right">7</td>
          <td style="text-align: right">32%</td>
          <td>2C / 0H / 2M / 3L</td>
          <td>$6.91</td>
          <td></td>
      </tr>
      <tr>
          <td>HSA-C</td>
          <td style="text-align: right">22</td>
          <td style="text-align: right">5</td>
          <td style="text-align: right">23%</td>
          <td>0C / 1H / 1M / 3L</td>
          <td>$9.70</td>
          <td></td>
      </tr>
  </tbody>
</table>
<p>The mixed model pipelines (HSA-A, HSA-B) retained a higher amount of critical issue candidates. Opus is more aggressive in disproving the identified issue candidates. The Opus only run was the most conservative and kept just five candidates, 23% of all that were identified by the Hunter.
It&rsquo;s surprising that, unlike in the single prompt baselines, the Hunter prompt in the three HSA runs resulted in Sonnet and Opus identifying the same number of candidates (22), showing how the prompt encourages broad exploration.</p>
<h3 id="vulnerability-candidates-by-type">Vulnerability candidates by type</h3>
<p>Looking at the vulnerability types (types merged using gpt-5.4 across findings for simplicity), we can check for the overlap of the baseline single prompt and the multi-prompt approach. Vulnerability candidates identified and confirmed are marked with ✅, those identified and disproven with ❌, and <code>–</code> means that this vulnerability type was not identified in the run.</p>
<table>
  <thead>
      <tr>
          <th>Vulnerability types</th>
          <th style="text-align: center">Sonnet</th>
          <th style="text-align: center">Opus</th>
          <th style="text-align: center">HSA-A</th>
          <th style="text-align: center">HSA-B</th>
          <th style="text-align: center">HSA-C</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Audio URL fetching / SSRF / URL validation</td>
          <td style="text-align: center">✅</td>
          <td style="text-align: center">✅</td>
          <td style="text-align: center">✅</td>
          <td style="text-align: center">✅</td>
          <td style="text-align: center">✅</td>
      </tr>
      <tr>
          <td>Authentication enforcement / auth bypass</td>
          <td style="text-align: center">✅</td>
          <td style="text-align: center">✅</td>
          <td style="text-align: center">✅</td>
          <td style="text-align: center">✅</td>
          <td style="text-align: center">❌</td>
      </tr>
      <tr>
          <td>Command/path handling</td>
          <td style="text-align: center">✅</td>
          <td style="text-align: center">✅</td>
          <td style="text-align: center">–</td>
          <td style="text-align: center">–</td>
          <td style="text-align: center">❌</td>
      </tr>
      <tr>
          <td>Container/runtime hardening</td>
          <td style="text-align: center">✅</td>
          <td style="text-align: center">✅</td>
          <td style="text-align: center">❌</td>
          <td style="text-align: center">✅</td>
          <td style="text-align: center">✅</td>
      </tr>
      <tr>
          <td>CORS / cross-origin policy</td>
          <td style="text-align: center">✅</td>
          <td style="text-align: center">✅</td>
          <td style="text-align: center">✅</td>
          <td style="text-align: center">❌</td>
          <td style="text-align: center">❌</td>
      </tr>
      <tr>
          <td>Credential exposure via query strings or logs</td>
          <td style="text-align: center">–</td>
          <td style="text-align: center">–</td>
          <td style="text-align: center">✅</td>
          <td style="text-align: center">✅</td>
          <td style="text-align: center">❌</td>
      </tr>
      <tr>
          <td>Cryptographic / identifier hygiene</td>
          <td style="text-align: center">✅</td>
          <td style="text-align: center">–</td>
          <td style="text-align: center">❌</td>
          <td style="text-align: center">❌</td>
          <td style="text-align: center">❌</td>
      </tr>
      <tr>
          <td>Error and client-facing info disclosure</td>
          <td style="text-align: center">✅</td>
          <td style="text-align: center">✅</td>
          <td style="text-align: center">✅</td>
          <td style="text-align: center">✅</td>
          <td style="text-align: center">✅</td>
      </tr>
      <tr>
          <td>HTTPS / secure transport enforcement</td>
          <td style="text-align: center">✅</td>
          <td style="text-align: center">–</td>
          <td style="text-align: center">✅</td>
          <td style="text-align: center">❌</td>
          <td style="text-align: center">–</td>
      </tr>
      <tr>
          <td>Logging / audit handling</td>
          <td style="text-align: center">✅</td>
          <td style="text-align: center">✅</td>
          <td style="text-align: center">✅</td>
          <td style="text-align: center">✅</td>
          <td style="text-align: center">❌</td>
      </tr>
      <tr>
          <td>Resource exhaustion / rate limiting</td>
          <td style="text-align: center">✅</td>
          <td style="text-align: center">✅</td>
          <td style="text-align: center">✅</td>
          <td style="text-align: center">✅</td>
          <td style="text-align: center">✅</td>
      </tr>
      <tr>
          <td>Security headers / browser hardening</td>
          <td style="text-align: center">✅</td>
          <td style="text-align: center">✅</td>
          <td style="text-align: center">❌</td>
          <td style="text-align: center">❌</td>
          <td style="text-align: center">❌</td>
      </tr>
      <tr>
          <td>Temp/static file handling</td>
          <td style="text-align: center">✅</td>
          <td style="text-align: center">✅</td>
          <td style="text-align: center">✅</td>
          <td style="text-align: center">❌</td>
          <td style="text-align: center">❌</td>
      </tr>
      <tr>
          <td>Token validation strength</td>
          <td style="text-align: center">✅</td>
          <td style="text-align: center">✅</td>
          <td style="text-align: center">❌</td>
          <td style="text-align: center">❌</td>
          <td style="text-align: center">❌</td>
      </tr>
      <tr>
          <td>WebSocket origin handling</td>
          <td style="text-align: center">–</td>
          <td style="text-align: center">–</td>
          <td style="text-align: center">–</td>
          <td style="text-align: center">–</td>
          <td style="text-align: center">✅</td>
      </tr>
  </tbody>
</table>
<p>The common vulnerability types across the runs relate to the file URL processing where there is no validation in the code, to resource exhaustion where the full file (max. 2GB) is being read into memory before being processed by ffmpeg. The error in client-facing information disclosure is about potential logging of information or exceptions without sanitization of data. The Opus-only pipeline (HSA-C) was filtering out vulnerabilities that would not be easy to exploit without having gained access to the server in the first place already.</p>
<p>With the output from the different runs, the hard work would begin, meaning the actual validation of the vulnerabilities, left as the exercise to the interested reader.</p>
<h2 id="summary">Summary</h2>
<p>If you&rsquo;re into vulnerability hunting, the post leaves you with more questions open than it answers. On purpose, I used a code example that does not have a ground truth solution readily available. This way, I expected lower bias through training data or vulnerability disclosures. It certainly would be interesting to use other model sets, for example the GPT-5.x or Qwen3.5 families, which I might try another time.</p>
<p>What did we learn from the experiment? Mixing models allows for broader exploration of vulnerability candidates at a lower cost, making the hunting stage a good candidate for using cheaper models. Using a more powerful model for validation of the candidates allows for quicker pruning of results that may not matter that much. To make good use of this property, system or environment-specific considerations would need to be encoded in the prompt(s), increasing the chances that the model is getting the right context, rather than resorting to averaging based on its training data. This problem-specific context could be given at least to the Arbiter.</p>
<p>I also found reading the detailed arguments from the Hunter and the Skeptic to be useful. These debate outcomes can help in understanding the analysis results and in prioritization. As such, the prompting technique of the Hunter, Skeptic, Arbiter trio can be applied to other problems that start with an exploration and that require scoring against a certain set of criteria. It&rsquo;s easy to imagine scenarios where bugs can be identified in a code base, verified by the Skeptic who even could be running code after mutating it to validate the bug, and where the Arbiter could be performing additional scoring against business criteria or value. Same can be applied to architectural design, starting with a persona exploring the key design considerations and making a draft. Then, we let the draft to be challenged by multiple persona who are trying to argue for simplicity, cost, or certain reference architectures/patterns. In this case, the Arbiter does not need to be making a final call on solution A or B, but rather scoring solutions using a scorecard or a set of criteria informed by architectural principles.</p>
]]></content:encoded>
    </item>
    <item>
      <title>MCP Sampling</title>
      <link>https://ocytko.net/posts/mcp-sampling/</link>
      <pubDate>Thu, 19 Feb 2026 00:00:00 +0000</pubDate>
      <guid>https://ocytko.net/posts/mcp-sampling/</guid>
      <description>This post explores MCP sampling and illustrates how important guardrails are to protect clients against malicious servers.</description>
      <content:encoded><![CDATA[<p>As <a href="https://ocytko.net/posts/mcp-servers-what-happens-behind-the-scenes/">indicated before</a>, I have been further exploring the constantly evolving MCP world. MCP sampling is a fun element of the <a href="https://modelcontextprotocol.io/specification/2025-11-25/client/sampling#sampling">Model Context Protocol</a>.
It essentially allows the server to access and use the client&rsquo;s LLM. This way the MCP server gets access to additional capabilities while offloading the cost to the user. While the protocol states:</p>
<blockquote>
<p><em>&ldquo;For trust &amp; safety and security, there <strong>SHOULD</strong> always be a human in the loop with the ability to deny sampling requests.&rdquo;</em></p>
</blockquote>
<p>surely there will be clients that do not implement any guardrails and there will be servers that will attempt to misuse the powers they have received access to.</p>
<p>Let&rsquo;s take a look at a simple MCP server and client. For serving the LLM we will use <a href="https://lmstudio.ai/">LM Studio</a> with <code>ministral-3-14b-instruct-2512</code> as model.</p>
<h3 id="server">Server</h3>
<p>The server uses <a href="https://github.com/prefecthq/fastmcp">FastMCPv2</a> and exposes a tool <code>generate_keywords</code>. The tool generates a list of keywords matching a given <code>topic</code>. The generation task is offloaded to the LLM of the client by invoking a helper function <code>send_sampling_message</code> to send the sampling requests.
When the tool is called, two sampling requests are sent in sequence:</p>
<ol>
<li>The first one takes the topic, constructs a prompt for the list generation, and then asks the client&rsquo;s LLM to rewrite it (in this case: restricting the generation to five words only).</li>
<li>The second one uses the client&rsquo;s LLM to actually execute the prompt (in this case: generating a list of keywords with restrictions from 1).</li>
</ol>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt"> 1
</span><span class="lnt"> 2
</span><span class="lnt"> 3
</span><span class="lnt"> 4
</span><span class="lnt"> 5
</span><span class="lnt"> 6
</span><span class="lnt"> 7
</span><span class="lnt"> 8
</span><span class="lnt"> 9
</span><span class="hl"><span class="lnt">10
</span></span><span class="lnt">11
</span><span class="lnt">12
</span><span class="lnt">13
</span><span class="lnt">14
</span><span class="lnt">15
</span><span class="lnt">16
</span><span class="lnt">17
</span><span class="lnt">18
</span><span class="lnt">19
</span><span class="lnt">20
</span><span class="lnt">21
</span><span class="lnt">22
</span><span class="lnt">23
</span><span class="lnt">24
</span><span class="lnt">25
</span><span class="lnt">26
</span><span class="hl"><span class="lnt">27
</span></span><span class="lnt">28
</span><span class="lnt">29
</span><span class="lnt">30
</span><span class="lnt">31
</span><span class="lnt">32
</span><span class="hl"><span class="lnt">33
</span></span><span class="lnt">34
</span><span class="lnt">35
</span><span class="lnt">36
</span><span class="lnt">37
</span><span class="lnt">38
</span><span class="lnt">39
</span><span class="lnt">40
</span><span class="lnt">41
</span><span class="lnt">42
</span><span class="lnt">43
</span><span class="lnt">44
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="cl"><span class="kn">import</span> <span class="nn">logging</span>
</span></span><span class="line"><span class="cl"><span class="kn">import</span> <span class="nn">sys</span>
</span></span><span class="line"><span class="cl"><span class="kn">from</span> <span class="nn">fastmcp</span> <span class="kn">import</span> <span class="n">FastMCP</span><span class="p">,</span> <span class="n">Context</span>
</span></span><span class="line"><span class="cl"><span class="kn">from</span> <span class="nn">mcp.types</span> <span class="kn">import</span> <span class="n">SamplingMessage</span><span class="p">,</span> <span class="n">TextContent</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="n">logging</span><span class="o">.</span><span class="n">getLogger</span><span class="p">()</span><span class="o">.</span><span class="n">setLevel</span><span class="p">(</span><span class="n">logging</span><span class="o">.</span><span class="n">INFO</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="n">mcp</span> <span class="o">=</span> <span class="n">FastMCP</span><span class="p">(</span><span class="n">name</span><span class="o">=</span><span class="s2">&#34;MCP sampling&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line hl"><span class="cl"><span class="k">async</span> <span class="k">def</span> <span class="nf">send_sampling_message</span><span class="p">(</span><span class="n">ctx</span><span class="p">:</span> <span class="n">Context</span><span class="p">,</span> <span class="n">message</span><span class="p">:</span> <span class="nb">str</span><span class="p">):</span>
</span></span><span class="line"><span class="cl">    <span class="k">return</span> <span class="k">await</span> <span class="n">ctx</span><span class="o">.</span><span class="n">sample</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">        <span class="n">messages</span><span class="o">=</span><span class="p">[</span>
</span></span><span class="line"><span class="cl">            <span class="n">SamplingMessage</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">                <span class="n">role</span><span class="o">=</span><span class="s2">&#34;user&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">                <span class="n">content</span><span class="o">=</span><span class="n">TextContent</span><span class="p">(</span><span class="nb">type</span><span class="o">=</span><span class="s2">&#34;text&#34;</span><span class="p">,</span> <span class="n">text</span><span class="o">=</span><span class="n">message</span><span class="p">),</span>
</span></span><span class="line"><span class="cl">            <span class="p">)</span>
</span></span><span class="line"><span class="cl">        <span class="p">],</span>
</span></span><span class="line"><span class="cl">        <span class="n">max_tokens</span><span class="o">=</span><span class="mi">1000</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="nd">@mcp.tool</span>
</span></span><span class="line"><span class="cl"><span class="k">async</span> <span class="k">def</span> <span class="nf">generate_keywords</span><span class="p">(</span><span class="n">topic</span><span class="p">:</span> <span class="nb">str</span><span class="p">,</span> <span class="n">ctx</span><span class="p">:</span> <span class="n">Context</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="nb">str</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">    <span class="s2">&#34;&#34;&#34;Generate keywords using LLM sampling.&#34;&#34;&#34;</span>
</span></span><span class="line"><span class="cl">    <span class="n">prompt</span> <span class="o">=</span> <span class="sa">f</span><span class="s2">&#34;List words that represent names of </span><span class="si">{</span><span class="n">topic</span><span class="si">}</span><span class="s2">&#34;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="c1"># 1. Prompt rewrite</span>
</span></span><span class="line hl"><span class="cl">    <span class="n">result</span> <span class="o">=</span> <span class="k">await</span> <span class="n">send_sampling_message</span><span class="p">(</span><span class="n">ctx</span><span class="p">,</span> <span class="sa">f</span><span class="s2">&#34;Rewrite &#39;</span><span class="si">{</span><span class="n">prompt</span><span class="si">}</span><span class="s2">&#39; to include a restriction for the list to only contain 5 words. Return the rewritten text only.&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="n">rewritten_prompt</span> <span class="o">=</span> <span class="n">result</span><span class="o">.</span><span class="n">text</span>
</span></span><span class="line"><span class="cl">    <span class="n">logging</span><span class="o">.</span><span class="n">info</span><span class="p">(</span><span class="sa">f</span><span class="s2">&#34;rewritten prompt: </span><span class="si">{</span><span class="n">rewritten_prompt</span><span class="si">}</span><span class="s2">&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="c1"># 2. Keyword generation</span>
</span></span><span class="line hl"><span class="cl">    <span class="n">result</span> <span class="o">=</span> <span class="k">await</span> <span class="n">send_sampling_message</span><span class="p">(</span><span class="n">ctx</span><span class="p">,</span> <span class="n">rewritten_prompt</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="n">logging</span><span class="o">.</span><span class="n">info</span><span class="p">(</span><span class="sa">f</span><span class="s2">&#34;sampling message: </span><span class="si">{</span><span class="n">result</span><span class="o">.</span><span class="n">text</span><span class="si">}</span><span class="s2">&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="k">return</span> <span class="n">result</span><span class="o">.</span><span class="n">text</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">def</span> <span class="nf">main</span><span class="p">():</span>
</span></span><span class="line"><span class="cl">    <span class="n">mcp</span><span class="o">.</span><span class="n">run</span><span class="p">(</span><span class="n">transport</span><span class="o">=</span><span class="s2">&#34;streamable-http&#34;</span><span class="p">,</span> <span class="n">port</span><span class="o">=</span><span class="mi">5001</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="n">sys</span><span class="o">.</span><span class="n">exit</span><span class="p">(</span><span class="mi">0</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">if</span> <span class="vm">__name__</span> <span class="o">==</span> <span class="s2">&#34;__main__&#34;</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">    <span class="n">main</span><span class="p">()</span>
</span></span></code></pre></td></tr></table>
</div>
</div><h3 id="client">Client</h3>
<p>The client is simple. It uses the <code>lmstudio</code> library for model access for convenience. This part could be easily replaced with any other model call.
All the magic is in the <code>sampling_handler</code> function, which processes the sampling request and triggers the LLM inference.</p>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt"> 1
</span><span class="lnt"> 2
</span><span class="lnt"> 3
</span><span class="lnt"> 4
</span><span class="lnt"> 5
</span><span class="lnt"> 6
</span><span class="lnt"> 7
</span><span class="lnt"> 8
</span><span class="lnt"> 9
</span><span class="lnt">10
</span><span class="lnt">11
</span><span class="lnt">12
</span><span class="lnt">13
</span><span class="lnt">14
</span><span class="hl"><span class="lnt">15
</span></span><span class="lnt">16
</span><span class="lnt">17
</span><span class="lnt">18
</span><span class="lnt">19
</span><span class="lnt">20
</span><span class="lnt">21
</span><span class="lnt">22
</span><span class="lnt">23
</span><span class="lnt">24
</span><span class="lnt">25
</span><span class="lnt">26
</span><span class="lnt">27
</span><span class="hl"><span class="lnt">28
</span></span><span class="lnt">29
</span><span class="lnt">30
</span><span class="lnt">31
</span><span class="lnt">32
</span><span class="lnt">33
</span><span class="lnt">34
</span><span class="lnt">35
</span><span class="lnt">36
</span><span class="lnt">37
</span><span class="lnt">38
</span><span class="lnt">39
</span><span class="lnt">40
</span><span class="lnt">41
</span><span class="lnt">42
</span><span class="lnt">43
</span><span class="lnt">44
</span><span class="lnt">45
</span><span class="lnt">46
</span><span class="lnt">47
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="cl"><span class="kn">import</span> <span class="nn">logging</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="kn">import</span> <span class="nn">lmstudio</span>
</span></span><span class="line"><span class="cl"><span class="kn">from</span> <span class="nn">fastmcp.client</span> <span class="kn">import</span> <span class="n">Client</span>
</span></span><span class="line"><span class="cl"><span class="kn">from</span> <span class="nn">fastmcp.client.sampling</span> <span class="kn">import</span> <span class="p">(</span>
</span></span><span class="line"><span class="cl">    <span class="n">SamplingMessage</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="n">SamplingParams</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="n">RequestContext</span><span class="p">,</span>
</span></span><span class="line"><span class="cl"><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="n">logging</span><span class="o">.</span><span class="n">getLogger</span><span class="p">()</span><span class="o">.</span><span class="n">setLevel</span><span class="p">(</span><span class="n">logging</span><span class="o">.</span><span class="n">INFO</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="n">llm</span> <span class="o">=</span> <span class="n">lmstudio</span><span class="o">.</span><span class="n">llm</span><span class="p">(</span><span class="s2">&#34;ministral-3-14b-instruct-2512&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line hl"><span class="cl"><span class="k">async</span> <span class="k">def</span> <span class="nf">sampling_handler</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">    <span class="n">messages</span><span class="p">:</span> <span class="nb">list</span><span class="p">[</span><span class="n">SamplingMessage</span><span class="p">],</span>
</span></span><span class="line"><span class="cl">    <span class="n">params</span><span class="p">:</span> <span class="n">SamplingParams</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="n">context</span><span class="p">:</span> <span class="n">RequestContext</span>
</span></span><span class="line"><span class="cl"><span class="p">)</span> <span class="o">-&gt;</span> <span class="nb">str</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">    <span class="s2">&#34;&#34;&#34;Accept sampling messages from the server and execute inference.&#34;&#34;&#34;</span>
</span></span><span class="line"><span class="cl">    <span class="n">logging</span><span class="o">.</span><span class="n">info</span><span class="p">(</span><span class="sa">f</span><span class="s2">&#34;sampling handler operation with message: </span><span class="si">{</span><span class="n">messages</span><span class="si">}</span><span class="s2">&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="n">message</span> <span class="o">=</span> <span class="s2">&#34;</span><span class="se">\n\n</span><span class="s2">&#34;</span><span class="o">.</span><span class="n">join</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">        <span class="p">[</span>
</span></span><span class="line"><span class="cl">            <span class="sa">f</span><span class="s2">&#34;</span><span class="si">{</span><span class="n">m</span><span class="o">.</span><span class="n">content</span><span class="o">.</span><span class="n">text</span><span class="si">}</span><span class="s2">&#34;</span> <span class="k">for</span> <span class="n">m</span> <span class="ow">in</span> <span class="n">messages</span>
</span></span><span class="line"><span class="cl">        <span class="p">]</span>
</span></span><span class="line"><span class="cl">    <span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line hl"><span class="cl">    <span class="n">response</span> <span class="o">=</span> <span class="n">llm</span><span class="o">.</span><span class="n">respond</span><span class="p">(</span><span class="n">message</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="n">logging</span><span class="o">.</span><span class="n">info</span><span class="p">(</span><span class="sa">f</span><span class="s2">&#34;LLM response: </span><span class="si">{</span><span class="n">response</span><span class="o">.</span><span class="n">content</span><span class="si">}</span><span class="s2">&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="k">return</span> <span class="n">response</span><span class="o">.</span><span class="n">content</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="n">client</span> <span class="o">=</span> <span class="n">Client</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">    <span class="s2">&#34;http://localhost:5001/mcp&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="n">sampling_handler</span><span class="o">=</span><span class="n">sampling_handler</span><span class="p">,</span>
</span></span><span class="line"><span class="cl"><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">async</span> <span class="k">def</span> <span class="nf">main</span><span class="p">():</span>
</span></span><span class="line"><span class="cl">    <span class="k">async</span> <span class="k">with</span> <span class="n">client</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">        <span class="k">await</span> <span class="n">client</span><span class="o">.</span><span class="n">ping</span><span class="p">()</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">        <span class="n">sampling_result</span> <span class="o">=</span> <span class="k">await</span> <span class="n">client</span><span class="o">.</span><span class="n">call_tool</span><span class="p">(</span><span class="s2">&#34;generate_keywords&#34;</span><span class="p">,</span> <span class="p">{</span><span class="s2">&#34;topic&#34;</span><span class="p">:</span> <span class="s2">&#34;elements&#34;</span><span class="p">})</span>
</span></span><span class="line"><span class="cl">        <span class="n">logging</span><span class="o">.</span><span class="n">info</span><span class="p">(</span><span class="sa">f</span><span class="s2">&#34;sampling result: &#39;</span><span class="si">{</span><span class="n">sampling_result</span><span class="o">.</span><span class="n">content</span><span class="p">[</span><span class="mi">0</span><span class="p">]</span><span class="o">.</span><span class="n">text</span><span class="si">}</span><span class="s2">&#39;&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">if</span> <span class="vm">__name__</span> <span class="o">==</span> <span class="s2">&#34;__main__&#34;</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">    <span class="kn">import</span> <span class="nn">asyncio</span>
</span></span><span class="line"><span class="cl">    <span class="n">asyncio</span><span class="o">.</span><span class="n">run</span><span class="p">(</span><span class="n">main</span><span class="p">())</span>
</span></span></code></pre></td></tr></table>
</div>
</div><p>To run the example code, we first start the server with <code>uv run server.py</code> and then run the client: <code>uv run client.py</code>.
The expected output is as follows.</p>
<p><strong>server</strong>:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sh" data-lang="sh"><span class="line"><span class="cl">INFO:root:rewritten prompt: <span class="s2">&#34;List **five** words that represent names of chemical elements.&#34;</span>
</span></span><span class="line"><span class="cl"><span class="o">[</span>...<span class="o">]</span>
</span></span><span class="line"><span class="cl">INFO:root:sampling message: Here are five words that represent names of chemical elements:
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">1. Oxygen
</span></span><span class="line"><span class="cl">2. Hydrogen
</span></span><span class="line"><span class="cl">3. Carbon
</span></span><span class="line"><span class="cl">4. Gold
</span></span><span class="line"><span class="cl">5. Sodium
</span></span></code></pre></div><p><strong>client</strong>:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sh" data-lang="sh"><span class="line"><span class="cl">INFO:root:Sampling handler operation with message: <span class="o">[</span>SamplingMessage<span class="o">(</span><span class="nv">role</span><span class="o">=</span><span class="s1">&#39;user&#39;</span>, <span class="nv">content</span><span class="o">=</span>TextContent<span class="o">(</span><span class="nv">type</span><span class="o">=</span><span class="s1">&#39;text&#39;</span>, <span class="nv">text</span><span class="o">=</span><span class="s2">&#34;Rewrite &#39;List words that represent names of elements&#39; to include a restriction for the list to only contain 5 words. Return the rewritten text only.&#34;</span>, <span class="nv">annotations</span><span class="o">=</span>None, <span class="nv">meta</span><span class="o">=</span>None<span class="o">))]</span>
</span></span><span class="line"><span class="cl"><span class="o">[</span>...<span class="o">]</span>
</span></span><span class="line"><span class="cl">INFO:root:Sampling handler operation with message: <span class="o">[</span>SamplingMessage<span class="o">(</span><span class="nv">role</span><span class="o">=</span><span class="s1">&#39;user&#39;</span>, <span class="nv">content</span><span class="o">=</span>TextContent<span class="o">(</span><span class="nv">type</span><span class="o">=</span><span class="s1">&#39;text&#39;</span>, <span class="nv">text</span><span class="o">=</span><span class="s1">&#39;&#34;List **five** words that represent names of chemical elements.&#34;&#39;</span>, <span class="nv">annotations</span><span class="o">=</span>None, <span class="nv">meta</span><span class="o">=</span>None<span class="o">))]</span>
</span></span><span class="line"><span class="cl"><span class="o">[</span>...<span class="o">]</span>
</span></span><span class="line"><span class="cl">INFO:root:Sampling result: <span class="s1">&#39;Here are five words that represent names of chemical elements:
</span></span></span><span class="line"><span class="cl"><span class="s1">
</span></span></span><span class="line"><span class="cl"><span class="s1">1. Oxygen
</span></span></span><span class="line"><span class="cl"><span class="s1">2. Hydrogen
</span></span></span><span class="line"><span class="cl"><span class="s1">3. Carbon
</span></span></span><span class="line"><span class="cl"><span class="s1">4. Gold
</span></span></span><span class="line"><span class="cl"><span class="s1">5. Sodium&#39;</span>
</span></span></code></pre></div><h3 id="communication-flow">Communication flow</h3>
<p>Below is the communication flow depicted as a sequence diagram.</p>
<figure class="align-center ">
    <img loading="lazy" src="mcp-sampling-seq.png#center"
         alt="Sequence diagram of the client server communication, incl. LLM."/> <figcaption>
            <p>Sequence diagram showing the client server communication including the LLM calls.</p>
        </figcaption>
</figure>

<h2 id="summary">Summary</h2>
<p>The example shows that the sampling handler is the key piece to pay attention to. For unattended execution of sampling requests, guardrails are a must.
The MCP server stays in full control how often and when to execute sampling. The client can inspect the prompt and decide whether to execute the sampling request or not. This shows how dangerous a malicious server can be when disguised as a helpful tool. Imagine, like in this example, that the server will trigger two sampling calls. The first call executes legitimate logic while the second just uses &ldquo;free&rdquo; model inference for requests collected in a queue.</p>
]]></content:encoded>
    </item>
    <item>
      <title>MCP servers: what happens behind the scenes?</title>
      <link>https://ocytko.net/posts/mcp-servers-what-happens-behind-the-scenes/</link>
      <pubDate>Sun, 06 Apr 2025 20:42:36 +0000</pubDate>
      <guid>https://ocytko.net/posts/mcp-servers-what-happens-behind-the-scenes/</guid>
      <description>The codebases of MCP servers tend to be early-stage API wrappers, far away in quality from existing SDKs. Non-determinism of LLMs adds…</description>
      <content:encoded><![CDATA[<p><em>Originally published on <a href="https://medium.com/@bocytko/mcp-servers-what-happens-behind-the-scenes-8d532f8b15fb">medium</a>.</em></p>
<p>If you are following any news related to LLMs, you surely have seen <a href="https://www.anthropic.com/news/model-context-protocol">Model Context Protocol</a> (MCP) getting a lot of attention lately. It also <a href="https://www.thoughtworks.com/radar/platforms/model-context-protocol-mcp">made it to ASSESS</a> in the recently released Tech Radar Vol. 32 by Thoughtworks which will further increase its visibility. In your feeds, MCP will be typically mentioned in conjunction with IDEs via Cursor, Windsurf, or VSCode with Github Copilot Chat by users who get access to new capabilities or extend the context of LLMs using MCPs. Often showcased by non-engineers who achieve results that were difficult for them to achieve without MCPs (such as generating scenes in Blender), their excitement is adding to the hype around MCP. Yet, when one explores the actual codebases of MCP servers, one finds early-stage, incomplete API wrappers that are far away from existing SDKs or libraries available to Software Engineers. Adding the non-determinism coming from LLMs, we’re in for a treat! 🍿</p>
<p>Having tried out the <a href="https://github.com/ahujasid/blender-mcp/">Blender integration</a> via <a href="https://docs.github.com/en/copilot/customizing-copilot/extending-copilot-chat-with-mcp">VSCode and Github Copilot</a> which was far too frequently running into rate limits, I was looking for a simple setup to try out MCP servers to understand what happens under the hood. I found <a href="https://github.com/openai/openai-agents-python">openai-agents-python</a> to provide a very simple setup for an experimentation harness against a chosen set of MCP servers. Its default integration with OpenAI’s Traces for observability provides a detailed look on what happens behind the scenes (it also saves me from running any <a href="https://ocytko.net/posts/opentelemetry-meets-openai-manual-instrumentation/">OpenTelemetry stack</a> I used previously).</p>
<p>In this post, I will be trying out three MCP servers calling them through <code>openai-agents-python</code> and using <code>gpt-4o</code> as model (default). The convenience of the framework is that given a configuration of MCP servers, it automatically handles their lifecycle (incl. download) without additional actions from the user.</p>
<h2 id="mcp-andllm">MCP and LLM</h2>
<p>The interplay between the LLM and MCP is quite simple. The LLM gets the user query and passes the list of available MCP functions (aka. tools) with their description and parameter specification to the LLM. The LLM then decides which function is most appropriate to call in the scenario defined by the user and returns as output the function call incl. parameters. The workflow orchestrator (e.g. IDE or agent framework) executes the function call using the MCP protocol and afterwards passes the input and output to the LLM for it to determine the next action.</p>
<h2 id="trying-out-mcp-server-git-git-repo-access-via-local-filesystem">Trying out mcp-server-git: Git repo access via local filesystem</h2>
<p>Starting point is the <a href="https://github.com/openai/openai-agents-python/blob/064e25b01b5c82c08aea66ff898ff27adbb013d8/examples/mcp/git_example/main.py">git_example</a> from <code>openai-agents-python</code> that we will use as basis for future experiments as well. The code sets up an agent with tools from <a href="https://github.com/modelcontextprotocol/servers/tree/main/src/git">mcp-server-git</a> for accessing any chosen repository from our local file system. The agent requests the most frequent contributor and asks for the summary of the latest change in the repo. After setting the <code>OPENAPI_API_KEY</code> environment variable, we can test it out.</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">$ <span class="nb">export</span> <span class="nv">OPENAPI_API_KEY</span><span class="o">=</span>sk-...
</span></span><span class="line"><span class="cl">$ uv run python main.py
</span></span><span class="line"><span class="cl">Please enter the path to the git repository: /tmp/openai-agents-python
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">----------------------------------------
</span></span><span class="line"><span class="cl">Running: Who<span class="err">&#39;</span>s the most frequent contributor?
</span></span><span class="line"><span class="cl">The most frequent contributor to the repository is **Rohan Mehta**.
</span></span><span class="line"><span class="cl">----------------------------------------
</span></span><span class="line"><span class="cl">Running: Summarize the last change in the repository.
</span></span><span class="line"><span class="cl">The last change in the repository was made by James Hills on 2025-04-04. The commit <span class="nb">hash</span> is <span class="sb">`</span>064e25b01b5c82c08aea66ff898ff27adbb013d8<span class="sb">`</span>, and the message was: <span class="s2">&#34;add links and mcp + voice examples (#438)&#34;</span>.
</span></span></code></pre></div><p>The first answer could be more comprehensive, but at least it worked out of the box. Let’s take a look what happened under the hood:</p>
<figure class="align-center ">
    <img loading="lazy" src="1_2jQUGAl3N5ajMYBRsHYAiw.png#center"
         alt="OpenAI Trace view for the main.py program. Shows MCP Tool list, LLM completion request to generate tool call, the git_log tool call, and LLM completion request to generate the final answer."/> <figcaption>
            <p>OpenAI Trace view for the main.py program. Shows MCP Tool list, LLM completion request to generate tool call, the git_log tool call, and LLM completion request to generate the final answer.</p>
        </figcaption>
</figure>

<p>We can see the agent fetching the list of MCP tools and calling the LLM that is then converting the user query to a tool call for <code>git_log</code>:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="cl"><span class="n">git_log</span><span class="p">({</span>
</span></span><span class="line"><span class="cl">  <span class="s2">&#34;repo_path&#34;</span><span class="p">:</span> <span class="s2">&#34;/tmp/openai-agents-python&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">  <span class="s2">&#34;max_count&#34;</span><span class="p">:</span> <span class="mi">1000</span>
</span></span><span class="line"><span class="cl"><span class="p">})</span>
</span></span></code></pre></div><p>As described before, the MCP server performs the tool call and passes the result again to the LLM. Since the git log contains user and commit data (commit_id, author, message) we end up with 29k tokens passed to the LLM after which we get the final response shown to the user.</p>
<figure class="align-center ">
    <img loading="lazy" src="1_yY523aRltMVTVEkpxlk2Bg.png#center"
         alt="Trace with git_log function call with input and output."/> <figcaption>
            <p>Trace with git_log function call with input and output.</p>
        </figcaption>
</figure>

<p><code>mcp-server-git</code> provides also operations to work on commits which are definitely interesting to try out another time.</p>
<h2 id="trying-out-github-mcp-server-remote-github-access-viaapi">Trying out github-mcp-server: remote Github access via API</h2>
<p>Now, let’s try the official <a href="https://github.com/github/github-mcp-server">github MCP server</a>, first released on April, 4th 2025. We will use the same queries as in the prior example, but specify the repo names directly in the prompts for the LLM to know what repository we want to query:</p>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt"> 1
</span><span class="lnt"> 2
</span><span class="lnt"> 3
</span><span class="lnt"> 4
</span><span class="lnt"> 5
</span><span class="lnt"> 6
</span><span class="lnt"> 7
</span><span class="lnt"> 8
</span><span class="lnt"> 9
</span><span class="lnt">10
</span><span class="lnt">11
</span><span class="lnt">12
</span><span class="lnt">13
</span><span class="lnt">14
</span><span class="lnt">15
</span><span class="lnt">16
</span><span class="lnt">17
</span><span class="lnt">18
</span><span class="lnt">19
</span><span class="lnt">20
</span><span class="lnt">21
</span><span class="lnt">22
</span><span class="lnt">23
</span><span class="lnt">24
</span><span class="lnt">25
</span><span class="lnt">26
</span><span class="lnt">27
</span><span class="lnt">28
</span><span class="lnt">29
</span><span class="lnt">30
</span><span class="lnt">31
</span><span class="lnt">32
</span><span class="lnt">33
</span><span class="lnt">34
</span><span class="lnt">35
</span><span class="lnt">36
</span><span class="lnt">37
</span><span class="lnt">38
</span><span class="lnt">39
</span><span class="lnt">40
</span><span class="lnt">41
</span><span class="lnt">42
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="cl"><span class="k">async</span> <span class="k">def</span> <span class="nf">send</span><span class="p">(</span><span class="n">agent</span><span class="p">:</span> <span class="n">Agent</span><span class="p">,</span> <span class="n">message</span><span class="p">:</span> <span class="nb">str</span><span class="p">):</span>
</span></span><span class="line"><span class="cl">    <span class="nb">print</span><span class="p">(</span><span class="s2">&#34;</span><span class="se">\\</span><span class="s2">n&#34;</span> <span class="o">+</span> <span class="s2">&#34;-&#34;</span> <span class="o">*</span> <span class="mi">40</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="nb">print</span><span class="p">(</span><span class="sa">f</span><span class="s2">&#34;Running: </span><span class="si">{</span><span class="n">message</span><span class="si">}</span><span class="s2">&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="n">result</span> <span class="o">=</span> <span class="k">await</span> <span class="n">Runner</span><span class="o">.</span><span class="n">run</span><span class="p">(</span><span class="n">starting_agent</span><span class="o">=</span><span class="n">agent</span><span class="p">,</span> <span class="nb">input</span><span class="o">=</span><span class="n">message</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="nb">print</span><span class="p">(</span><span class="n">result</span><span class="o">.</span><span class="n">final_output</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">async</span> <span class="k">def</span> <span class="nf">run</span><span class="p">(</span><span class="n">mcp_server</span><span class="p">:</span> <span class="n">MCPServer</span><span class="p">):</span>
</span></span><span class="line"><span class="cl">    <span class="n">agent</span> <span class="o">=</span> <span class="n">Agent</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">        <span class="n">name</span><span class="o">=</span><span class="s2">&#34;Assistant&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">        <span class="n">instructions</span><span class="o">=</span><span class="sa">f</span><span class="s2">&#34;Answer questions about Git repositories.&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">        <span class="n">mcp_servers</span><span class="o">=</span><span class="p">[</span><span class="n">mcp_server</span><span class="p">],</span>
</span></span><span class="line"><span class="cl">    <span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="n">cont</span> <span class="o">=</span> <span class="kc">True</span>
</span></span><span class="line"><span class="cl">    <span class="k">while</span><span class="p">(</span><span class="n">cont</span><span class="p">):</span>
</span></span><span class="line"><span class="cl">        <span class="n">message</span> <span class="o">=</span> <span class="nb">input</span><span class="p">(</span><span class="s2">&#34;&gt; Input: &#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">        <span class="c1"># exit upon Exit or CTRL+C</span>
</span></span><span class="line"><span class="cl">        <span class="k">if</span> <span class="n">message</span> <span class="o">==</span> <span class="s2">&#34;Exit&#34;</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">            <span class="n">cont</span> <span class="o">=</span> <span class="kc">False</span>
</span></span><span class="line"><span class="cl">            <span class="k">break</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">        <span class="k">await</span> <span class="n">send</span><span class="p">(</span><span class="n">agent</span><span class="p">,</span> <span class="n">message</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">async</span> <span class="k">def</span> <span class="nf">main</span><span class="p">():</span>
</span></span><span class="line"><span class="cl">    <span class="k">async</span> <span class="k">with</span> <span class="n">MCPServerStdio</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">        <span class="n">cache_tools_list</span><span class="o">=</span><span class="kc">True</span>
</span></span><span class="line"><span class="cl">        <span class="n">params</span><span class="o">=</span><span class="p">{</span>
</span></span><span class="line"><span class="cl">            <span class="s2">&#34;command&#34;</span><span class="p">:</span> <span class="s2">&#34;docker&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">            <span class="s2">&#34;args&#34;</span><span class="p">:</span> <span class="p">[</span>
</span></span><span class="line"><span class="cl">                <span class="s2">&#34;run&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">                <span class="s2">&#34;-i&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">                <span class="s2">&#34;--rm&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">                <span class="s2">&#34;-e&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">                <span class="s2">&#34;GITHUB_PERSONAL_ACCESS_TOKEN&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">                <span class="s2">&#34;ghcr.io/github/github-mcp-server&#34;</span>
</span></span><span class="line"><span class="cl">            <span class="p">],</span>
</span></span><span class="line"><span class="cl">            <span class="s2">&#34;env&#34;</span><span class="p">:</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">                <span class="s2">&#34;GITHUB_PERSONAL_ACCESS_TOKEN&#34;</span><span class="p">:</span> <span class="n">os</span><span class="o">.</span><span class="n">getenv</span><span class="p">(</span><span class="s2">&#34;GITHUB_PERSONAL_ACCESS_TOKEN&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">            <span class="p">}</span>
</span></span><span class="line"><span class="cl">        <span class="p">},</span>
</span></span><span class="line"><span class="cl">    <span class="p">)</span> <span class="k">as</span> <span class="n">server</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">        <span class="k">with</span> <span class="n">trace</span><span class="p">(</span><span class="n">workflow_name</span><span class="o">=</span><span class="s2">&#34;MCP Git (Official)&#34;</span><span class="p">):</span>
</span></span><span class="line"><span class="cl">            <span class="k">await</span> <span class="n">run</span><span class="p">(</span><span class="n">server</span><span class="p">)</span>
</span></span></code></pre></td></tr></table>
</div>
</div><p>At time of writing, the MCP server provides 29 functions (e.g. issue, commit, pull request operations and search) which wrap the Github APIs called using a Github token defined as an environment variable <code>GITHUB_PERSONAL_ACCESS_TOKEN</code>. For our experiment, in theory, just the function <code>list_commits()</code> for listing commits would be sufficient to answer the posed questions. Let’s see how successful the agent will be with this (simple?) task.</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">$ <span class="nb">export</span> <span class="nv">GITHUB_PERSONAL_ACCESS_TOKEN</span><span class="o">=</span>...
</span></span><span class="line"><span class="cl">$ uv run python github-mcp-server.py
</span></span><span class="line"><span class="cl">GitHub MCP Server running on stdio
</span></span><span class="line"><span class="cl">----------------------------------------
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">&gt; Input: Who<span class="s1">&#39;s the most frequent contributor to zalando/skipper?
</span></span></span><span class="line"><span class="cl"><span class="s1">&gt; Running: Who&#39;</span>s the most frequent contributor to zalando/skipper?
</span></span><span class="line"><span class="cl">Error invoking MCP tool search_users: failed to search users: GET &lt;https://api.github.com/search/users?order<span class="o">=</span>desc<span class="p">&amp;</span><span class="nv">page</span><span class="o">=</span>1<span class="p">&amp;</span><span class="nv">per_page</span><span class="o">=</span>1<span class="p">&amp;</span><span class="nv">q</span><span class="o">=</span>repo%3Azalando%2Fskipper<span class="p">&amp;</span><span class="nv">sort</span><span class="o">=</span>repositories:&gt; <span class="m">422</span> Validation Failed <span class="o">[{</span>Resource:Search Field:q Code:invalid Message:None of the search qualifiers apply to this search type.<span class="o">}]</span>
</span></span></code></pre></div><p>Looks like we hit a bug: the query constructed by the LLM is invalid (reported as <a href="https://github.com/github/github-mcp-server/issues/135">github/github-mcp-server#135</a>). The LLM opted for <code>search_users()</code> instead of using the git commit log like our prior experiment.</p>
<p>Let’s take a look at the second query:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">$ uv run python github-mcp-server.py
</span></span><span class="line"><span class="cl">GitHub MCP Server running on stdio
</span></span><span class="line"><span class="cl">----------------------------------------
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">&gt; Input: Summarize the last change in the repository zalando/skipper
</span></span><span class="line"><span class="cl">&gt; Running: Summarize the last change in the repository zalando/skipper
</span></span><span class="line"><span class="cl">Error getting response: Error code: <span class="m">429</span> - <span class="o">{</span><span class="s1">&#39;error&#39;</span>: <span class="o">{</span><span class="s1">&#39;message&#39;</span>: <span class="s1">&#39;Request too large for gpt-4o in organization org-XYZ on tokens per min (TPM): Limit 30000, Requested 68487. The input or output tokens must be reduced in order to run successfully. Visit &lt;https://platform.openai.com/account/rate-limits&gt; to learn more.&#39;</span>, <span class="s1">&#39;type&#39;</span>: <span class="s1">&#39;tokens&#39;</span>, <span class="s1">&#39;param&#39;</span>: None, <span class="s1">&#39;code&#39;</span>: <span class="s1">&#39;rate_limit_exceeded&#39;</span><span class="o">}}</span>. <span class="o">(</span>request_id: req_abcabcabc...<span class="o">)</span>
</span></span></code></pre></div><figure class="align-center ">
    <img loading="lazy" src="1_9Z8X4vTdRJg7WUEL_VblAQ.png#center"
         alt="Error message from OpenAI: rate limit of 30000 tokens exceeded, requested 68487 tokens."/> <figcaption>
            <p>Error message from OpenAI: rate limit of 30000 tokens exceeded, requested 68487 tokens.</p>
        </figcaption>
</figure>

<p>Another error. This time we hit a rate limit given that the request to the LLM is too large (68487 tokens vs. 30000 being the limit). It took the agent only 19.40s to realize this… Something must have inflated the response from the MCP server. A closer look at the traces reveals that the tool call to <code>list_commits</code> with <code>perPage = 1</code> resulted in a long response (containing 30 commits which is the default setting) with a whooping size of 180KB. Bug number two — filed as <a href="https://github.com/github/github-mcp-server/issues/136">github/github-mcp-server#136</a>.</p>
<p>A closer look at the MCP server response also shows the excessive payload originating from the Github API response. A single commit object has 5–6 KB and beyond information on the commit it includes goodies such as the PGP signature of the author, hardly needed for the task at hand:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-json" data-lang="json"><span class="line"><span class="cl"><span class="p">{</span>
</span></span><span class="line"><span class="cl">  <span class="nt">&#34;node_id&#34;</span><span class="p">:</span> <span class="s2">&#34;C_kwDOAlA7-NoAKDdlMmNhM2JmZDI2NzNiNTFkYjRhYmNmYmQ1OWRlYTMzYTk0YzIwMzE&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">  <span class="nt">&#34;sha&#34;</span><span class="p">:</span> <span class="s2">&#34;7e2ca3bfd2673b51db4abcfbd59dea33a94c2031&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">  <span class="nt">&#34;commit&#34;</span><span class="p">:</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="nt">&#34;author&#34;</span><span class="p">:</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">      <span class="nt">&#34;date&#34;</span><span class="p">:</span> <span class="s2">&#34;2025-04-02T09:43:35Z&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">      <span class="nt">&#34;name&#34;</span><span class="p">:</span> <span class="s2">&#34;... ...&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">      <span class="nt">&#34;email&#34;</span><span class="p">:</span> <span class="s2">&#34;...&#34;</span>
</span></span><span class="line"><span class="cl">    <span class="p">},</span>
</span></span><span class="line"><span class="cl">    <span class="nt">&#34;committer&#34;</span><span class="p">:</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">      <span class="nt">&#34;date&#34;</span><span class="p">:</span> <span class="s2">&#34;2025-04-02T09:43:35Z&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">      <span class="nt">&#34;name&#34;</span><span class="p">:</span> <span class="s2">&#34;GitHub&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">      <span class="nt">&#34;email&#34;</span><span class="p">:</span> <span class="s2">&#34;noreply@github.com&#34;</span>
</span></span><span class="line"><span class="cl">    <span class="p">},</span>
</span></span><span class="line"><span class="cl">    <span class="nt">&#34;message&#34;</span><span class="p">:</span> <span class="s2">&#34;Add context to log entry (#3466)\\n\\nThis change enriches log entry with request context to be used by custom log formatter.\\n\\nSigned-off-by: ... ... &lt;....&gt;&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="nt">&#34;tree&#34;</span><span class="p">:</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">      <span class="nt">&#34;sha&#34;</span><span class="p">:</span> <span class="s2">&#34;cff0e8e12633313bff1291df86f65f3f81acfd66&#34;</span>
</span></span><span class="line"><span class="cl">    <span class="p">},</span>
</span></span><span class="line"><span class="cl">    <span class="nt">&#34;url&#34;</span><span class="p">:</span> <span class="s2">&#34;&lt;https://api.github.com/repos/zalando/skipper/git/commits/7e2ca3bfd2673b51db4abcfbd59dea33a94c2031&gt;&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="nt">&#34;verification&#34;</span><span class="p">:</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">      <span class="nt">&#34;verified&#34;</span><span class="p">:</span> <span class="kc">true</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">      <span class="nt">&#34;reason&#34;</span><span class="p">:</span> <span class="s2">&#34;valid&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">      <span class="nt">&#34;signature&#34;</span><span class="p">:</span> <span class="s2">&#34;-----BEGIN PGP SIGNATURE-----\\n\\n[...]\\n-----END PGP SIGNATURE-----\\n&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">      <span class="nt">&#34;payload&#34;</span><span class="p">:</span> <span class="s2">&#34;tree cff0e8e12633313bff1291df86f65f3f81acfd66\\nparent 985da0b03d2fa499e4868a22d85426ed387b4a98\\nauthor ... ... &lt;...&gt; 1743587015 +0200\\ncommitter GitHub &lt;noreply@github.com&gt; 1743587015 +0200\\n\\nAdd context to log entry (#3466)\\n\\nThis change enriches log entry with request context to be used by custom log formatter.\\n\\nSigned-off-by: ... ... &lt;...&gt;&#34;</span>
</span></span><span class="line"><span class="cl">    <span class="p">},</span>
</span></span><span class="line"><span class="cl">    <span class="nt">&#34;comment_count&#34;</span><span class="p">:</span> <span class="mi">0</span>
</span></span><span class="line"><span class="cl">  <span class="p">},</span>
</span></span><span class="line"><span class="cl">  <span class="nt">&#34;author&#34;</span><span class="p">:</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="nt">&#34;login&#34;</span><span class="p">:</span> <span class="s2">&#34;...&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="err">[...]</span>
</span></span><span class="line"><span class="cl">    <span class="nt">&#34;subscriptions_url&#34;</span><span class="p">:</span> <span class="s2">&#34;&lt;https://api.github.com/users/.../subscriptions&gt;&#34;</span>
</span></span><span class="line"><span class="cl">  <span class="p">},</span>
</span></span><span class="line"><span class="cl">  <span class="err">[...]</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span></code></pre></div><p>BTW. If we adjust the query for our most frequent committer to <code>Who's the most frequent contributor to zalando/skipper? Use list_commits tool to answer this question.</code> we will run of course into the same error. However, fetching 30 commits is hardly close to the right answer.</p>
<h2 id="trying-out-aws-documentation-mcp-search-for-public-awsdocs">Trying out aws-documentation-mcp: search for public AWS docs</h2>
<p>Let’s take a look at another freshly released MCP servers — this time <a href="https://awslabs.github.io/mcp/">from AWS</a>. I choose the simplest one to run: <code>aws-documentation-mcp</code> provides access to public AWS docs and does not require any AWS account access.</p>
<p>A closer look at the <a href="https://github.com/awslabs/mcp/blob/main/src/aws-documentation-mcp-server/awslabs/aws_documentation_mcp_server/server.py">server code</a> shows the magic behind this server. It’s running a search against the <a href="https://github.com/awslabs/mcp/blob/b61e292c24343fe577d6bbdbf07eb059820642c2/src/aws-documentation-mcp-server/awslabs/aws_documentation_mcp_server/server.py#L34">AWS documentation search endpoint</a> (<a href="https://proxy.search.docs.aws.amazon.com/search">https://proxy.search.docs.aws.amazon.com/search</a>) with a dedicated <a href="https://github.com/awslabs/mcp/blob/b61e292c24343fe577d6bbdbf07eb059820642c2/src/aws-documentation-mcp-server/awslabs/aws_documentation_mcp_server/server.py#L33">user agent</a> <code>[...] ModelContextProtocol/1.0 (AWS Documentation Server)</code> allowing AWS to track the MCP server use (and likely rate limit usage). The system prompt also <a href="https://github.com/awslabs/mcp/blob/b61e292c24343fe577d6bbdbf07eb059820642c2/src/aws-documentation-mcp-server/awslabs/aws_documentation_mcp_server/server.py#L50">mentions a recommendation tool</a> to use for retrieving related content. The MCP can perform a search using the user’s input and next use search results to fetch the content, parse it using <code>beautifulsoup4</code> and convert into markdown using <code>markdownify</code>.</p>
<p>So far so good. Let’s try it out with two questions about S3:</p>
<ul>
<li>“What are the file size limits for AWS S3?”</li>
<li>“How does one enable S3 transfer acceleration?”</li>
</ul>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt"> 1
</span><span class="lnt"> 2
</span><span class="lnt"> 3
</span><span class="lnt"> 4
</span><span class="lnt"> 5
</span><span class="lnt"> 6
</span><span class="lnt"> 7
</span><span class="lnt"> 8
</span><span class="lnt"> 9
</span><span class="lnt">10
</span><span class="lnt">11
</span><span class="lnt">12
</span><span class="lnt">13
</span><span class="lnt">14
</span><span class="lnt">15
</span><span class="lnt">16
</span><span class="lnt">17
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="cl"><span class="k">async</span> <span class="k">def</span> <span class="nf">run</span><span class="p">(</span><span class="n">mcp_server</span><span class="p">:</span> <span class="n">MCPServer</span><span class="p">):</span>
</span></span><span class="line"><span class="cl">    <span class="n">agent</span> <span class="o">=</span> <span class="n">Agent</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">        <span class="n">name</span><span class="o">=</span><span class="s2">&#34;Assistant&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">        <span class="n">instructions</span><span class="o">=</span><span class="sa">f</span><span class="s2">&#34;Answer questions about AWS services.&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">        <span class="n">mcp_servers</span><span class="o">=</span><span class="p">[</span><span class="n">mcp_server</span><span class="p">],</span>
</span></span><span class="line"><span class="cl">    <span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="k">await</span> <span class="n">send</span><span class="p">(</span><span class="n">agent</span><span class="p">,</span> <span class="s2">&#34;What are the file size limits for AWS S3?&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="k">await</span> <span class="n">send</span><span class="p">(</span><span class="n">agent</span><span class="p">,</span> <span class="s2">&#34;How does one enable S3 transfer acceleration?&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">async</span> <span class="k">def</span> <span class="nf">main</span><span class="p">():</span>
</span></span><span class="line"><span class="cl">    <span class="k">async</span> <span class="k">with</span> <span class="n">MCPServerStdio</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">        <span class="n">cache_tools_list</span><span class="o">=</span><span class="kc">True</span><span class="p">,</span>  <span class="c1"># Cache the tools list, for demonstration</span>
</span></span><span class="line"><span class="cl">        <span class="n">params</span><span class="o">=</span><span class="p">{</span><span class="s2">&#34;command&#34;</span><span class="p">:</span> <span class="s2">&#34;uvx&#34;</span><span class="p">,</span> <span class="s2">&#34;args&#34;</span><span class="p">:</span> <span class="p">[</span><span class="s2">&#34;awslabs.aws-documentation-mcp-server@latest&#34;</span><span class="p">]},</span>
</span></span><span class="line"><span class="cl">    <span class="p">)</span> <span class="k">as</span> <span class="n">server</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">        <span class="k">with</span> <span class="n">trace</span><span class="p">(</span><span class="n">workflow_name</span><span class="o">=</span><span class="s2">&#34;MCP AWS Documentation Server&#34;</span><span class="p">):</span>
</span></span><span class="line"><span class="cl">            <span class="k">await</span> <span class="n">run</span><span class="p">(</span><span class="n">server</span><span class="p">)</span>
</span></span></code></pre></td></tr></table>
</div>
</div><p>The task took about 24 seconds, but it looks like a success:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-markdown" data-lang="markdown"><span class="line"><span class="cl">$ uv run python aws-docs.py
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">----------------------------------------
</span></span><span class="line"><span class="cl"><span class="k">&gt; </span><span class="ge">Running: What are the file size limits for AWS S3?
</span></span></span><span class="line"><span class="cl">Here are the file size limits for uploading objects to Amazon S3:
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">-</span> **Console Upload**: Up to 160 GB per file using the Amazon S3 console.
</span></span><span class="line"><span class="cl"><span class="k">-</span> **Single PUT Operation**: Up to 5 GB per file using AWS SDKs, REST API, or AWS CLI.
</span></span><span class="line"><span class="cl"><span class="k">-</span> **Multipart Upload**: Up to 5 TB per file using AWS SDKs, REST API, or AWS CLI. This option is for larger files, allowing uploads in parts ranging from 5 MB to 5 TB.
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">You can use multipart uploads for efficient handling of larger objects.
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">----------------------------------------
</span></span><span class="line"><span class="cl"><span class="k">&gt; </span><span class="ge">Running: How does one enable S3 transfer acceleration?
</span></span></span><span class="line"><span class="cl">To enable S3 Transfer Acceleration, you have several options including using the AWS Management Console, AWS CLI, or API. Here&#39;s a concise guide:
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="gu">### Using the AWS Management Console
</span></span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">1.</span> <span class="gs">**Sign In**</span>: Log into the AWS Management Console and open the S3 interface.
</span></span><span class="line"><span class="cl"><span class="k">2.</span> <span class="gs">**Select Bucket**</span>: In the left navigation pane, choose <span class="gs">**General purpose buckets**</span>. Then, select the bucket you want to enable transfer acceleration for.
</span></span><span class="line"><span class="cl"><span class="k">3.</span> <span class="gs">**Access Properties**</span>: Click on <span class="gs">**Properties**</span>.
</span></span><span class="line"><span class="cl"><span class="k">4.</span> <span class="gs">**Edit Transfer Acceleration**</span>: Under <span class="gs">**Transfer acceleration**</span>, click <span class="gs">**Edit**</span>.
</span></span><span class="line"><span class="cl"><span class="k">5.</span> <span class="gs">**Enable**</span>: Choose <span class="gs">**Enable**</span>, then save changes.
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="gu">### Using the AWS CLI
</span></span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">To enable Transfer Acceleration using AWS CLI, run the following command:
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="s">```bash
</span></span></span><span class="line"><span class="cl">aws s3api put-bucket-accelerate-configuration --bucket <span class="o">[</span>bucket-name<span class="o">]</span> --accelerate-configuration <span class="nv">Status</span><span class="o">=</span>Enabled
</span></span><span class="line"><span class="cl"><span class="s">```</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="gu">### Using the Accelerated Endpoint
</span></span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">Once enabled, you can use the accelerated endpoint for faster data transfers:
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">-</span> Find the <span class="gs">**Accelerated endpoint**</span> under the bucket&#39;s <span class="gs">**Properties**</span> tab.
</span></span><span class="line"><span class="cl"><span class="k">-</span> Use <span class="sb">`s3-accelerate.amazonaws.com`</span> to direct requests through the accelerated endpoint.
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">For more detailed instructions and examples of using the AWS CLI and SDKs, visit the [<span class="nt">Amazon S3 Transfer Acceleration documentation</span>](<span class="na">https://docs.aws.amazon.com/AmazonS3/latest/userguide/transfer-acceleration-examples.html</span>).
</span></span></code></pre></div><p>The traces reveal what happened behind the scenes. For each of the inputs, we get <code>search_documentation</code> followed by <code>read_documentation</code>.</p>
<figure class="align-center ">
    <img loading="lazy" src="1_M2PEJvHL7oKU6-G3WRSjdQ.png#center"
         alt="Trace for MCP AWS Documentation Server: two tool calls and two LLM calls."/> <figcaption>
            <p>Trace for MCP AWS Documentation Server: two tool calls and two LLM calls.</p>
        </figcaption>
</figure>

<p>This time, the limits are correctly respected and by default, the search gets the top5 results.</p>
<figure class="align-center ">
    <img loading="lazy" src="1_pgZrI0gM_Zg2guAuMnti2Q.png#center"
         alt="Trace for a search_documentation tool call with limit of 5 and ranked results with rank_order, url, and title."/> <figcaption>
            <p>Trace for a search_documentation tool call with limit of 5 and ranked results with rank_order, url, and title.</p>
        </figcaption>
</figure>

<p>Also, we get valid (though simplified) answers straight from the docs. The queries took 14.88s and 10.61s respectively, which is on the slow end, yet if the MCP is embedded into the IDE, likely convenient to access without resorting to using the browser.</p>
<h2 id="closing-words-to-mcp-to-notmcp">Closing words: to MCP to not MCP?</h2>
<p>What did we learn from this exploration? MCP servers are easy to set up and are a promising tool to equip agentic flows and assistants with access to additional information and capabilities. They also provide us with a way to bring local data as context for LLMs, especially in cases where indexing this data would be impractical. We also learned that the generated LLM calls are rather costly to execute due to excessive context size (if they get executed at all). Before using MCP servers, it’s important to verify the cost footprint for expected tasks as well as add monitoring and spend limits accordingly.</p>
<p>Building a good MCP server is far from easy: getting LLMs to generate the right and syntactically correct function calls is difficult and becomes more complex the more functions are available to choose from. We saw in the AWS example how detailed instructions help guide the model to generate a multi-step call flow required for the job. Without these instructions, we just can’t magically expect good results. It is easier to ask models to generate code that includes API calls instead.</p>
<p>In the git examples, to calculate the most frequent committer, one would expect an iteration over the commit list over at last a few pages. Yet currently, due to excessive size of the response, we cannot even parse 30 commits due to limits in the model’s context size (or model rate limits). This makes the approach rather impractical. A simple prompt: <code>Generate python code to interact with the Github API to determine the most frequent committer for a chosen repository.</code> results in a code snippet that does the job in a more predictable way. Similarly, <code>Generate git command to calculate the most frequent committer for a chosen repository.</code> returns <code>git shortlog -sne</code> that paired with <code>| head -n 1</code> returns the result for our local git repo. Needless to say how cheap these LLM calls are when compared to the MCP approach. Let’s see how the mentioned git MCP servers will evolve and for how long it will stay around. They’re early on their journey and require more work to be really useful and give users confidence when and how they will actually work. Field filtering and evals come to mind as first extensions that will be a major step change in quality.</p>
<p>I will certainly continue my experiments as the level of hype is too high for MCPs to disappear quickly.</p>
]]></content:encoded>
    </item>
    <item>
      <title>OpenTelemetry meets OpenAI: manual instrumentation</title>
      <link>https://ocytko.net/posts/opentelemetry-meets-openai-manual-instrumentation/</link>
      <pubDate>Thu, 04 May 2023 18:49:16 +0000</pubDate>
      <guid>https://ocytko.net/posts/opentelemetry-meets-openai-manual-instrumentation/</guid>
      <description>The posts explores three approaches to manual OpenTelemetry instrumentation for OpenAI calls in Langchain and LlamaIndex.</description>
      <content:encoded><![CDATA[<p><em>Originally published on <a href="https://medium.com/@bocytko/opentelemetry-meets-openai-manual-instrumentation-d103140c67e0">medium</a>.</em></p>
<p>In the <a href="https://ocytko.net/posts/opentelemetry-meets-openai/">previous post</a>, we inspected calls to OpenAI APIs triggered within <a href="https://github.com/hwchase17/langchain">Langchain</a> and <a href="https://github.com/jerryjliu/llama_index">LlamaIndex</a> by using OpenTelemetry auto-instrumentation. The spans shown in Jaeger UI were nice to see, but were missing rich information that is expected from a proper instrumentation approach. In this post, we will explore how to enrich spans with additional information using <a href="https://opentelemetry.io/docs/instrumentation/python/getting-started/#add-manual-instrumentation-to-automatic-instrumentation">manual instrumentation</a>.</p>
<h2 id="manual-instrumentation">Manual instrumentation</h2>
<p>OpenTelemetry provides means to add additional attributes to spans. The OpenTelemetry standard defines two rules:</p>
<blockquote>
<ol>
<li>Keys must be non-null string values</li>
<li>Values must be a non-null string, boolean, floating point value, integer, or an array of these values</li>
</ol>
</blockquote>
<p>Additionally, most commonly used fields follow naming conventions and are referred to as <a href="https://opentelemetry.io/docs/reference/specification/trace/semantic_conventions/">semantic attributes</a>.</p>
<p><em><strong>Note</strong>: Beware of adding fields that may contain PII information to span context. Unless you guarantee that all systems processing the telemetry drop stored data after a fixed period of time (e.g. 30 days), you may run into challenges related to privacy regulation, such as GDPR and its ‘Right to be forgotten’.</em></p>
<h2 id="adding-instrumentation-to-owncode">Adding instrumentation to own code</h2>
<p>Instrumenting own code is as simple as shown in the code below. It starts a new span called <code>function_name</code> with an attribute <code>arg</code> with value <code>42</code>.</p>
<p>Any spans that are added using auto-instrumentation to functions called by <code>function_name</code> will automatically become its child spans.</p>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt">1
</span><span class="lnt">2
</span><span class="lnt">3
</span><span class="lnt">4
</span><span class="lnt">5
</span><span class="lnt">6
</span><span class="lnt">7
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="cl"><span class="kn">from</span> <span class="nn">opentelemetry</span> <span class="kn">import</span> <span class="n">trace</span>
</span></span><span class="line"><span class="cl"><span class="p">(</span><span class="o">...</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="n">tracer</span> <span class="o">=</span> <span class="n">trace</span><span class="o">.</span><span class="n">get_tracer</span><span class="p">(</span><span class="vm">__name__</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="k">with</span> <span class="n">tracer</span><span class="o">.</span><span class="n">start_as_current_span</span><span class="p">(</span><span class="s2">&#34;function_name&#34;</span><span class="p">)</span> <span class="k">as</span> <span class="n">span</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">    <span class="n">arg</span> <span class="o">=</span> <span class="mi">42</span>
</span></span><span class="line"><span class="cl">    <span class="n">span</span><span class="o">.</span><span class="n">set_attribute</span><span class="p">(</span><span class="s2">&#34;arg&#34;</span><span class="p">,</span> <span class="n">arg</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="n">result</span> <span class="o">=</span> <span class="n">function_name</span><span class="p">(</span><span class="n">arg</span><span class="p">)</span>
</span></span></code></pre></td></tr></table>
</div>
</div><p>Alternatively, one can use the provided decorator, which results in simpler code in case it’s not necessary to capture any attributes in the spans.</p>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt">1
</span><span class="lnt">2
</span><span class="lnt">3
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="cl"><span class="nd">@tracer.start_as_current_span</span><span class="p">(</span><span class="s2">&#34;foobar&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="k">def</span> <span class="nf">foobar</span><span class="p">(</span><span class="n">arg</span><span class="p">):</span>
</span></span><span class="line"><span class="cl">    <span class="n">result</span> <span class="o">=</span> <span class="n">foo_bar</span><span class="p">(</span><span class="n">arg</span><span class="p">)</span>
</span></span></code></pre></td></tr></table>
</div>
</div><h2 id="adding-instrumentation-to-langchains-llmchains">Adding instrumentation to Langchain’s LLM Chains</h2>
<p>Langchain offers <a href="https://python.langchain.com/en/latest/modules/callbacks/getting_started.html#creating-and-using-a-custom-callbackhandler">Custom Callback Handlers</a> as means to execute additional functions in well-defined stages of the chains. To collect statistics on the prompts and token usage from the LLM calls, we can add spans in the <code>on_llm_start</code> and <code>on_llm_end</code> calls:</p>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt"> 1
</span><span class="lnt"> 2
</span><span class="lnt"> 3
</span><span class="lnt"> 4
</span><span class="lnt"> 5
</span><span class="lnt"> 6
</span><span class="lnt"> 7
</span><span class="lnt"> 8
</span><span class="lnt"> 9
</span><span class="lnt">10
</span><span class="lnt">11
</span><span class="lnt">12
</span><span class="lnt">13
</span><span class="lnt">14
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="cl"><span class="k">def</span> <span class="nf">on_llm_start</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">        <span class="bp">self</span><span class="p">,</span> <span class="n">serialized</span><span class="p">:</span> <span class="n">Dict</span><span class="p">[</span><span class="nb">str</span><span class="p">,</span> <span class="n">Any</span><span class="p">],</span> <span class="n">prompts</span><span class="p">:</span> <span class="n">List</span><span class="p">[</span><span class="nb">str</span><span class="p">],</span> <span class="o">**</span><span class="n">kwargs</span><span class="p">:</span> <span class="n">Any</span>
</span></span><span class="line"><span class="cl">    <span class="p">)</span> <span class="o">-&gt;</span> <span class="kc">None</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">        <span class="k">with</span> <span class="n">tracer</span><span class="o">.</span><span class="n">start_as_current_span</span><span class="p">(</span><span class="s2">&#34;on_llm_start&#34;</span><span class="p">)</span> <span class="k">as</span> <span class="n">span</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">            <span class="n">prompts_len</span> <span class="o">+=</span> <span class="nb">sum</span><span class="p">([</span><span class="nb">len</span><span class="p">(</span><span class="n">prompt</span><span class="p">)</span> <span class="k">for</span> <span class="n">prompt</span> <span class="ow">in</span> <span class="n">prompts</span><span class="p">])</span>
</span></span><span class="line"><span class="cl">            <span class="n">span</span><span class="o">.</span><span class="n">set_attribute</span><span class="p">(</span><span class="s2">&#34;num_processed_prompts&#34;</span><span class="p">,</span> <span class="nb">len</span><span class="p">(</span><span class="n">prompts</span><span class="p">))</span>
</span></span><span class="line"><span class="cl">            <span class="n">span</span><span class="o">.</span><span class="n">set_attribute</span><span class="p">(</span><span class="s2">&#34;prompts_len&#34;</span><span class="p">,</span> <span class="n">prompts_len</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">def</span> <span class="nf">on_llm_end</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">response</span><span class="p">:</span> <span class="n">LLMResult</span><span class="p">,</span> <span class="o">**</span><span class="n">kwargs</span><span class="p">:</span> <span class="n">Any</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="kc">None</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">        <span class="k">with</span> <span class="n">tracer</span><span class="o">.</span><span class="n">start_as_current_span</span><span class="p">(</span><span class="s2">&#34;on_llm_end&#34;</span><span class="p">)</span> <span class="k">as</span> <span class="n">span</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">            <span class="c1"># example output: {&#39;completion_tokens&#39;: 14, &#39;prompt_tokens&#39;: 71, &#39;total_tokens&#39;: 85}</span>
</span></span><span class="line"><span class="cl">            <span class="n">token_usage</span> <span class="o">=</span> <span class="n">response</span><span class="o">.</span><span class="n">llm_output</span><span class="p">[</span><span class="s2">&#34;token_usage&#34;</span><span class="p">]</span>
</span></span><span class="line"><span class="cl">            <span class="k">for</span> <span class="n">k</span><span class="p">,</span> <span class="n">v</span> <span class="ow">in</span> <span class="n">token_usage</span><span class="o">.</span><span class="n">items</span><span class="p">():</span>
</span></span><span class="line"><span class="cl">                <span class="n">span</span><span class="o">.</span><span class="n">set_attribute</span><span class="p">(</span><span class="n">k</span><span class="p">,</span> <span class="n">v</span><span class="p">)</span>
</span></span></code></pre></td></tr></table>
</div>
</div><h2 id="adding-instrumentation-for-openai-embeddings-in-llamaindex">Adding instrumentation for OpenAI Embeddings in LlamaIndex</h2>
<p>LlamaIndex does not provide callback mechanisms for its <a href="https://gpt-index.readthedocs.io/en/latest/how_to/customization/embeddings.html#how-are-embeddings-generated">embeddings</a> functions. Instead, we can to extend the <code>OpenAIEmbedding</code> class, include instrumentation code in the overridden methods, and pass an instance of this class to the relevant methods of the library. In the added spans we collect the text lengths as span attributes.</p>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt"> 1
</span><span class="lnt"> 2
</span><span class="lnt"> 3
</span><span class="lnt"> 4
</span><span class="lnt"> 5
</span><span class="lnt"> 6
</span><span class="lnt"> 7
</span><span class="lnt"> 8
</span><span class="lnt"> 9
</span><span class="lnt">10
</span><span class="lnt">11
</span><span class="lnt">12
</span><span class="lnt">13
</span><span class="lnt">14
</span><span class="lnt">15
</span><span class="lnt">16
</span><span class="lnt">17
</span><span class="lnt">18
</span><span class="lnt">19
</span><span class="lnt">20
</span><span class="lnt">21
</span><span class="lnt">22
</span><span class="lnt">23
</span><span class="lnt">24
</span><span class="lnt">25
</span><span class="lnt">26
</span><span class="lnt">27
</span><span class="lnt">28
</span><span class="lnt">29
</span><span class="lnt">30
</span><span class="lnt">31
</span><span class="lnt">32
</span><span class="lnt">33
</span><span class="lnt">34
</span><span class="lnt">35
</span><span class="lnt">36
</span><span class="lnt">37
</span><span class="lnt">38
</span><span class="lnt">39
</span><span class="lnt">40
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="cl"><span class="k">class</span> <span class="nc">InstrumentingOpenAIEmbedding</span><span class="p">(</span><span class="n">OpenAIEmbedding</span><span class="p">):</span>
</span></span><span class="line"><span class="cl">    <span class="k">def</span> <span class="fm">__init__</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">        <span class="bp">self</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">        <span class="n">mode</span><span class="p">:</span> <span class="nb">str</span> <span class="o">=</span> <span class="n">OpenAIEmbeddingMode</span><span class="o">.</span><span class="n">TEXT_SEARCH_MODE</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">        <span class="n">model</span><span class="p">:</span> <span class="nb">str</span> <span class="o">=</span> <span class="n">OpenAIEmbeddingModelType</span><span class="o">.</span><span class="n">TEXT_EMBED_ADA_002</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">        <span class="n">deployment_name</span><span class="p">:</span> <span class="n">Optional</span><span class="p">[</span><span class="nb">str</span><span class="p">]</span> <span class="o">=</span> <span class="kc">None</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">        <span class="o">**</span><span class="n">kwargs</span><span class="p">:</span> <span class="n">Any</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="p">)</span> <span class="o">-&gt;</span> <span class="kc">None</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">        <span class="s2">&#34;&#34;&#34;Init params.&#34;&#34;&#34;</span>
</span></span><span class="line"><span class="cl">        <span class="nb">super</span><span class="p">()</span><span class="o">.</span><span class="fm">__init__</span><span class="p">(</span><span class="o">**</span><span class="n">kwargs</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">        <span class="bp">self</span><span class="o">.</span><span class="n">mode</span> <span class="o">=</span> <span class="n">OpenAIEmbeddingMode</span><span class="p">(</span><span class="n">mode</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">        <span class="bp">self</span><span class="o">.</span><span class="n">model</span> <span class="o">=</span> <span class="n">OpenAIEmbeddingModelType</span><span class="p">(</span><span class="n">model</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">        <span class="bp">self</span><span class="o">.</span><span class="n">deployment_name</span> <span class="o">=</span> <span class="n">deployment_name</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="k">def</span> <span class="nf">_get_query_embedding</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">query</span><span class="p">:</span> <span class="nb">str</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="n">List</span><span class="p">[</span><span class="nb">float</span><span class="p">]:</span>
</span></span><span class="line"><span class="cl">        <span class="k">with</span> <span class="n">tracer</span><span class="o">.</span><span class="n">start_as_current_span</span><span class="p">(</span><span class="s2">&#34;_get_query_embedding&#34;</span><span class="p">)</span> <span class="k">as</span> <span class="n">span</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">            <span class="n">span</span><span class="o">.</span><span class="n">set_attribute</span><span class="p">(</span><span class="s2">&#34;query_length&#34;</span><span class="p">,</span> <span class="nb">len</span><span class="p">(</span><span class="n">query</span><span class="p">))</span>
</span></span><span class="line"><span class="cl">            <span class="k">return</span> <span class="nb">super</span><span class="p">()</span><span class="o">.</span><span class="n">_get_query_embedding</span><span class="p">(</span><span class="n">query</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="k">def</span> <span class="nf">_get_text_embedding</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">text</span><span class="p">:</span> <span class="nb">str</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="n">List</span><span class="p">[</span><span class="nb">float</span><span class="p">]:</span>
</span></span><span class="line"><span class="cl">        <span class="k">with</span> <span class="n">tracer</span><span class="o">.</span><span class="n">start_as_current_span</span><span class="p">(</span><span class="s2">&#34;_get_text_embedding&#34;</span><span class="p">)</span> <span class="k">as</span> <span class="n">span</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">            <span class="n">span</span><span class="o">.</span><span class="n">set_attribute</span><span class="p">(</span><span class="s2">&#34;text_length&#34;</span><span class="p">,</span> <span class="nb">len</span><span class="p">(</span><span class="n">text</span><span class="p">))</span>
</span></span><span class="line"><span class="cl">            <span class="k">return</span> <span class="nb">super</span><span class="p">()</span><span class="o">.</span><span class="n">_get_text_embedding</span><span class="p">(</span><span class="n">text</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="k">async</span> <span class="k">def</span> <span class="nf">_aget_text_embedding</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">text</span><span class="p">:</span> <span class="nb">str</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="n">List</span><span class="p">[</span><span class="nb">float</span><span class="p">]:</span>
</span></span><span class="line"><span class="cl">        <span class="k">with</span> <span class="n">tracer</span><span class="o">.</span><span class="n">start_as_current_span</span><span class="p">(</span><span class="s2">&#34;_aget_text_embedding&#34;</span><span class="p">)</span> <span class="k">as</span> <span class="n">span</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">            <span class="n">span</span><span class="o">.</span><span class="n">set_attribute</span><span class="p">(</span><span class="s2">&#34;text_length&#34;</span><span class="p">,</span> <span class="nb">len</span><span class="p">(</span><span class="n">text</span><span class="p">))</span>
</span></span><span class="line"><span class="cl">            <span class="n">embeddings</span> <span class="o">=</span> <span class="k">await</span> <span class="nb">super</span><span class="p">()</span><span class="o">.</span><span class="n">_aget_text_embedding</span><span class="p">(</span><span class="n">text</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">            <span class="k">return</span> <span class="n">embeddings</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="k">def</span> <span class="nf">_get_text_embeddings</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">texts</span><span class="p">:</span> <span class="n">List</span><span class="p">[</span><span class="nb">str</span><span class="p">])</span> <span class="o">-&gt;</span> <span class="n">List</span><span class="p">[</span><span class="n">List</span><span class="p">[</span><span class="nb">float</span><span class="p">]]:</span>
</span></span><span class="line"><span class="cl">        <span class="k">with</span> <span class="n">tracer</span><span class="o">.</span><span class="n">start_as_current_span</span><span class="p">(</span><span class="s2">&#34;_get_text_embeddings&#34;</span><span class="p">)</span> <span class="k">as</span> <span class="n">span</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">            <span class="n">span</span><span class="o">.</span><span class="n">set_attribute</span><span class="p">(</span><span class="s2">&#34;texts_len&#34;</span><span class="p">,</span> <span class="nb">sum</span><span class="p">([</span><span class="nb">len</span><span class="p">(</span><span class="n">txt</span><span class="p">)</span> <span class="k">for</span> <span class="n">txt</span> <span class="ow">in</span> <span class="n">texts</span><span class="p">]))</span>
</span></span><span class="line"><span class="cl">            <span class="k">return</span> <span class="nb">super</span><span class="p">()</span><span class="o">.</span><span class="n">_get_text_embeddings</span><span class="p">(</span><span class="n">texts</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="k">async</span> <span class="k">def</span> <span class="nf">_aget_text_embeddings</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">texts</span><span class="p">:</span> <span class="n">List</span><span class="p">[</span><span class="nb">str</span><span class="p">])</span> <span class="o">-&gt;</span> <span class="n">List</span><span class="p">[</span><span class="n">List</span><span class="p">[</span><span class="nb">float</span><span class="p">]]:</span>
</span></span><span class="line"><span class="cl">        <span class="k">with</span> <span class="n">tracer</span><span class="o">.</span><span class="n">start_as_current_span</span><span class="p">(</span><span class="s2">&#34;_aget_text_embeddings&#34;</span><span class="p">)</span> <span class="k">as</span> <span class="n">span</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">            <span class="n">span</span><span class="o">.</span><span class="n">set_attribute</span><span class="p">(</span><span class="s2">&#34;texts_len&#34;</span><span class="p">,</span> <span class="nb">sum</span><span class="p">([</span><span class="nb">len</span><span class="p">(</span><span class="n">txt</span><span class="p">)</span> <span class="k">for</span> <span class="n">txt</span> <span class="ow">in</span> <span class="n">texts</span><span class="p">]))</span>
</span></span><span class="line"><span class="cl">            <span class="n">embeddings</span> <span class="o">=</span> <span class="k">await</span> <span class="nb">super</span><span class="p">()</span><span class="o">.</span><span class="n">_aget_text_embeddings</span><span class="p">(</span><span class="n">texts</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">            <span class="k">return</span> <span class="n">embeddings</span>
</span></span></code></pre></td></tr></table>
</div>
</div><p>The obvious downside of the approach is that the code needs to be kept in sync with the extended base class, which results in increased maintenance effort in case of library upgrades.</p>
<h2 id="inspecting-thespans">Inspecting the spans</h2>
<p>Running and using the code mentioned earlier produces two traces. First, the embedding span with the added attribute <code>texts_len</code>:</p>
<figure class="align-center ">
    <img loading="lazy" src="1_XPEH0ygNLxYu0uTo_r1usA.png#center"
         alt="Screenshot from Jaeger UI showing the added embedding span with added attribute texts_len."/> <figcaption>
            <p>Screenshot from Jaeger UI showing the added embedding span with its attributes</p>
        </figcaption>
</figure>

<p>Next, the embedding traces and <code>on_llm_start</code> and <code>on_llm_end</code> traces with the captured <code>query_length</code> and token usage attributes:</p>
<figure class="align-center ">
    <img loading="lazy" src="1_8n-j13Hjix_CpTViQm8OTA.png#center"
         alt="Screenshot from Jaeger UI showing the embedding traces and on_llm_start and on_llm_end traces with the captured query_length and token usage attributes."/> <figcaption>
            <p>Screenshot from Jaeger UI showing the captured traces and LLM token usage attributes</p>
        </figcaption>
</figure>

<h2 id="writing-an-instrumentor-for-openai-embeddings-in-llamaindex">Writing an Instrumentor for OpenAI Embeddings in LlamaIndex</h2>
<p>Extending classes can be cumbersome and an unnecessary maintenance overhead. The built-in instrumentation offered by many of the <a href="https://github.com/open-telemetry/opentelemetry-python-contrib/tree/main/instrumentation">OpenTelemetry instrumentation packages for Python</a> offer inspiration for a different approach of instrumentation using function wrappers.</p>
<p>Following the example of the <a href="https://github.com/open-telemetry/opentelemetry-python-contrib/tree/main/instrumentation/opentelemetry-instrumentation-redis">Redis instrumentation library</a>, we use the convenient <a href="https://pypi.org/project/wrapt/">wrapt</a> package to write a simple wrapper function for three methods in the <code>OpenAIEmbedding</code> class. The wrapper <code>_traced</code> calculates the length of the passed string(s) depending on the function’s argument type (<code>str</code> or <code>List[str]</code>).</p>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt"> 1
</span><span class="lnt"> 2
</span><span class="lnt"> 3
</span><span class="lnt"> 4
</span><span class="lnt"> 5
</span><span class="lnt"> 6
</span><span class="lnt"> 7
</span><span class="lnt"> 8
</span><span class="lnt"> 9
</span><span class="lnt">10
</span><span class="lnt">11
</span><span class="lnt">12
</span><span class="lnt">13
</span><span class="lnt">14
</span><span class="lnt">15
</span><span class="lnt">16
</span><span class="lnt">17
</span><span class="lnt">18
</span><span class="lnt">19
</span><span class="lnt">20
</span><span class="lnt">21
</span><span class="lnt">22
</span><span class="lnt">23
</span><span class="lnt">24
</span><span class="lnt">25
</span><span class="lnt">26
</span><span class="lnt">27
</span><span class="lnt">28
</span><span class="lnt">29
</span><span class="lnt">30
</span><span class="lnt">31
</span><span class="lnt">32
</span><span class="lnt">33
</span><span class="lnt">34
</span><span class="lnt">35
</span><span class="lnt">36
</span><span class="lnt">37
</span><span class="lnt">38
</span><span class="lnt">39
</span><span class="lnt">40
</span><span class="lnt">41
</span><span class="lnt">42
</span><span class="lnt">43
</span><span class="lnt">44
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="cl"><span class="kn">from</span> <span class="nn">wrapt</span> <span class="kn">import</span> <span class="n">wrap_function_wrapper</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="kn">from</span> <span class="nn">llama_index.embeddings.openai</span> <span class="kn">import</span> <span class="n">OpenAIEmbedding</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="kn">from</span> <span class="nn">opentelemetry</span> <span class="kn">import</span> <span class="n">trace</span>
</span></span><span class="line"><span class="cl"><span class="kn">from</span> <span class="nn">opentelemetry.instrumentation.instrumentor</span> <span class="kn">import</span> <span class="n">BaseInstrumentor</span>
</span></span><span class="line"><span class="cl"><span class="kn">from</span> <span class="nn">opentelemetry.instrumentation.utils</span> <span class="kn">import</span> <span class="n">unwrap</span>
</span></span><span class="line"><span class="cl"><span class="kn">from</span> <span class="nn">opentelemetry.trace</span> <span class="kn">import</span> <span class="n">SpanKind</span><span class="p">,</span> <span class="n">Tracer</span><span class="p">,</span> <span class="n">get_tracer</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">def</span> <span class="nf">_instrument</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">    <span class="n">tracer</span><span class="p">:</span> <span class="n">Tracer</span>
</span></span><span class="line"><span class="cl"><span class="p">):</span>
</span></span><span class="line"><span class="cl">    <span class="k">def</span> <span class="nf">_traced</span><span class="p">(</span><span class="n">func</span><span class="p">,</span> <span class="n">instance</span><span class="p">,</span> <span class="n">args</span><span class="p">,</span> <span class="n">kwargs</span><span class="p">):</span>
</span></span><span class="line"><span class="cl">        <span class="k">with</span> <span class="n">tracer</span><span class="o">.</span><span class="n">start_as_current_span</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">            <span class="s2">&#34;get_embedding&#34;</span><span class="p">,</span> <span class="n">kind</span><span class="o">=</span><span class="n">SpanKind</span><span class="o">.</span><span class="n">CLIENT</span>
</span></span><span class="line"><span class="cl">        <span class="p">)</span> <span class="k">as</span> <span class="n">span</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">            <span class="k">if</span> <span class="n">span</span><span class="o">.</span><span class="n">is_recording</span><span class="p">():</span>
</span></span><span class="line"><span class="cl">                <span class="k">if</span> <span class="nb">len</span><span class="p">(</span><span class="n">args</span><span class="p">)</span> <span class="o">&gt;</span> <span class="mi">0</span> <span class="ow">and</span> <span class="n">args</span><span class="p">[</span><span class="mi">0</span><span class="p">]:</span>
</span></span><span class="line"><span class="cl">                    <span class="k">if</span> <span class="nb">isinstance</span><span class="p">(</span><span class="n">args</span><span class="p">[</span><span class="mi">0</span><span class="p">],</span> <span class="nb">list</span><span class="p">):</span>
</span></span><span class="line"><span class="cl">                        <span class="n">span</span><span class="o">.</span><span class="n">set_attribute</span><span class="p">(</span><span class="s2">&#34;text_length&#34;</span><span class="p">,</span> <span class="nb">sum</span><span class="p">(</span><span class="nb">len</span><span class="p">(</span><span class="n">e</span><span class="p">)</span> <span class="k">for</span> <span class="n">e</span> <span class="ow">in</span> <span class="n">args</span><span class="p">[</span><span class="mi">0</span><span class="p">]))</span>
</span></span><span class="line"><span class="cl">                    <span class="k">else</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">                        <span class="n">span</span><span class="o">.</span><span class="n">set_attribute</span><span class="p">(</span><span class="s2">&#34;text_length&#34;</span><span class="p">,</span> <span class="nb">len</span><span class="p">(</span><span class="n">args</span><span class="p">[</span><span class="mi">0</span><span class="p">]))</span>
</span></span><span class="line"><span class="cl">         
</span></span><span class="line"><span class="cl">            <span class="n">response</span> <span class="o">=</span> <span class="n">func</span><span class="p">(</span><span class="o">*</span><span class="n">args</span><span class="p">,</span> <span class="o">**</span><span class="n">kwargs</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">            <span class="k">return</span> <span class="n">response</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="n">wrap_function_wrapper</span><span class="p">(</span><span class="s2">&#34;llama_index.embeddings.openai&#34;</span><span class="p">,</span> <span class="s2">&#34;OpenAIEmbedding.get_query_embedding&#34;</span><span class="p">,</span> <span class="n">_traced</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="n">wrap_function_wrapper</span><span class="p">(</span><span class="s2">&#34;llama_index.embeddings.openai&#34;</span><span class="p">,</span> <span class="s2">&#34;OpenAIEmbedding.get_text_embedding&#34;</span><span class="p">,</span> <span class="n">_traced</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="n">wrap_function_wrapper</span><span class="p">(</span><span class="s2">&#34;llama_index.embeddings.openai&#34;</span><span class="p">,</span> <span class="s2">&#34;OpenAIEmbedding._get_text_embeddings&#34;</span><span class="p">,</span> <span class="n">_traced</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">class</span> <span class="nc">OpenAIEmbeddingInstrumentor</span><span class="p">(</span><span class="n">BaseInstrumentor</span><span class="p">):</span>
</span></span><span class="line"><span class="cl">    <span class="k">def</span> <span class="nf">instrumentation_dependencies</span><span class="p">(</span><span class="bp">self</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="n">Collection</span><span class="p">[</span><span class="nb">str</span><span class="p">]:</span>
</span></span><span class="line"><span class="cl">        <span class="k">return</span> <span class="p">(</span><span class="s2">&#34;llama-index ~= 0.4.32&#34;</span><span class="p">,)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="k">def</span> <span class="nf">_instrument</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="o">**</span><span class="n">kwargs</span><span class="p">):</span>
</span></span><span class="line"><span class="cl">        <span class="s2">&#34;&#34;&#34;Instruments llama-index module&#34;&#34;&#34;</span>
</span></span><span class="line"><span class="cl">        <span class="n">tracer_provider</span> <span class="o">=</span> <span class="n">kwargs</span><span class="o">.</span><span class="n">get</span><span class="p">(</span><span class="s2">&#34;tracer_provider&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">        <span class="n">tracer</span> <span class="o">=</span> <span class="n">get_tracer</span><span class="p">(</span><span class="vm">__name__</span><span class="p">,</span> <span class="s2">&#34;custom-tracer-version&#34;</span><span class="p">,</span> <span class="n">tracer_provider</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">        <span class="n">_instrument</span><span class="p">(</span><span class="n">tracer</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="k">def</span> <span class="nf">_uninstrument</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="o">**</span><span class="n">kwargs</span><span class="p">):</span>
</span></span><span class="line"><span class="cl">        <span class="n">unwrap</span><span class="p">(</span><span class="n">OpenAIEmbedding</span><span class="p">,</span> <span class="s2">&#34;get_query_embedding&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">        <span class="n">unwrap</span><span class="p">(</span><span class="n">OpenAIEmbedding</span><span class="p">,</span> <span class="s2">&#34;get_text_embedding&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">        <span class="n">unwrap</span><span class="p">(</span><span class="n">OpenAIEmbedding</span><span class="p">,</span> <span class="s2">&#34;_get_text_embeddings&#34;</span><span class="p">)</span>
</span></span></code></pre></td></tr></table>
</div>
</div><p>To ensure the instrumentor is actually used, it needs to be initialized with <code>OpenAIEmbeddingInstrumentor().instrument()</code> before the first library calls are initiated. The resulting traces generated by the instrumentor code are as follows:</p>
<figure class="align-center ">
    <img loading="lazy" src="1_iyVR5EJrXuZkuMBNcbtcDQ.png#center"
         alt="Screenshot from JaegerUI depicting the span generated by the generic OpenAIEmbeddingInstrumentor."/> <figcaption>
            <p>Screenshot from JaegerUI depicting the span generated by the generic <code>OpenAIEmbeddingInstrumentor</code></p>
        </figcaption>
</figure>

<h2 id="summary">Summary</h2>
<p>We explored adding additional context to spans by adding instrumentation in three different ways: (1) manual instrumentation of individual function calls, (2) extending classes to override methods with ones that include tracing code, (3) instrumenting library code using function wrappers. When to use which approach is highly contextual and depends on the use case at hand. Approach 1 is best used for one’s own code, approach 3 for instrumenting libraries, and approach 2 when a high degree of control over instrumentation is required.</p>
<p>It’s important to be careful and not <a href="https://opentelemetry.io/docs/concepts/instrumenting-library/#what-to-instrument">overdo instrumentation</a> and rely on the provided instrumentation packages whenever applicable. When considering adding manual instrumentation, it’s important to balance the benefits of additional detail with the potential complexity it may introduce. Note that in production deployments, tracing data is often <a href="https://opentelemetry.io/docs/concepts/sampling/">sampled</a> to deal with high data volume and keep the tracing cost footprint in check and this <a href="https://www.heinrichhartmann.com/sampling/">affects the accuracy</a> of the collected data.</p>
<h2 id="references">References</h2>
<ul>
<li><a href="https://opentelemetry.io/docs/instrumentation/python/getting-started/#add-manual-instrumentation-to-automatic-instrumentation">OpenTelemetry for Python: Adding manual instrumentation to automatic instrumentation</a></li>
<li><a href="https://opentelemetry.io/docs/instrumentation/python/manual/#tracing">OpenTelemetry for Python: Tracing</a></li>
<li><a href="https://opentelemetry.io/docs/reference/specification/trace/semantic_conventions/">OpenTelemetry: Trace Semantic Conventions</a></li>
<li><a href="https://opentelemetry.io/docs/concepts/instrumenting-library/#what-to-instrument">OpenTelemetry: What to instrument</a></li>
<li><a href="https://github.com/open-telemetry/opentelemetry-python-contrib/tree/main/instrumentation">OpenTelemetry: Python instrumentation libraries</a></li>
<li><a href="https://github.com/open-telemetry/opentelemetry-python-contrib/tree/main/instrumentation/opentelemetry-instrumentation-redis">OpenTelemetry: Redis Instrumentation library</a></li>
</ul>
]]></content:encoded>
    </item>
    <item>
      <title>OpenTelemetry meets OpenAI</title>
      <link>https://ocytko.net/posts/opentelemetry-meets-openai/</link>
      <pubDate>Sun, 23 Apr 2023 18:57:12 +0000</pubDate>
      <guid>https://ocytko.net/posts/opentelemetry-meets-openai/</guid>
      <description>Using automatic instrumentation to quickly assess which APIs are called by popular Python LLM demo apps.</description>
      <content:encoded><![CDATA[<p><em>Originally published on <a href="https://medium.com/@bocytko/opentelemetry-meets-openai-95f873aa2e41">medium</a>.</em></p>
<p>The “Hello World” of LLMs is the <a href="https://developers.llamaindex.ai/python/framework/understanding/putting_it_all_together/chatbots/building_a_chatbot/">Q&amp;A Knowledge Base</a> use case where the app creates embeddings for source documents and feeds a vector storage. Next, for each query it calculates the embedding, fetches top N documents from the vector store and uses the question and knowledge snippets to prompt the GPT-3/4 language models. Some vector stores are 3rd party stores (<a href="https://www.pinecone.io/">Pinecone</a>, <a href="https://weaviate.io/">Weaviate</a>), others in-memory (<a href="https://github.com/facebookresearch/faiss">FAISS</a>, <a href="https://github.com/jerryjliu/llama_index/blob/c54a6f2bf661135a54a75ffe90b69427d468aa34/gpt_index/indices/vector_store/vector_indices.py#L39">GPTSimpleVectorIndex</a>).</p>
<p>With the sheer amount of these and other demos and applications using popular libraries like <a href="https://github.com/hwchase17/langchain">langchain</a> and <a href="https://github.com/jerryjliu/llama_index">llama-index</a>, the question arises how to analyze new demos to quickly understand what APIs are being called and in which order? How frequently and with which latency? Checking code is tedious and provides only half the answers. Proper Telemetry tooling will help us discover the API endpoints called by the applications.</p>
<h2 id="automatic-instrumentation-for-python-applications">Automatic Instrumentation for Python applications</h2>
<p><a href="https://opentelemetry.io/">OpenTelemetry</a> provides <a href="https://opentelemetry.io/docs/instrumentation/python/automatic/">Automatic Instrumentation for Python</a> that comes to the rescue here. Using the Python agent that is attached to the application, it dynamically injects bytecode to capture telemetry from popular libraries and frameworks. The Langchain, OpenAI, and LlamaIndex libraries use Python’s <code>requests</code> under the hood, so we’ll need to make sure to use <a href="https://github.com/open-telemetry/opentelemetry-python-contrib/tree/main/instrumentation/opentelemetry-instrumentation-requests">opentelemetry-instrumentation-requests</a> explicitly.</p>
<p>To visualize the telemetry, we need to setup some tools first. There is a <a href="https://github.com/open-telemetry/opentelemetry-collector-contrib/tree/main/examples/demo">convenient demo</a> that ships with a docker-compose to boot up the collector and a few more tools. We ignore the two chatty demo apps that run in the background, though those are helpful to check if the telemetry setup works correctly.</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-shell" data-lang="shell"><span class="line"><span class="cl">$ git clone git@github.com:open-telemetry/opentelemetry-collector-contrib.git
</span></span><span class="line"><span class="cl">$ <span class="nb">cd</span> opentelemetry-collector-contrib/examples/demo
</span></span><span class="line"><span class="cl">$ docker-compose up -d     
</span></span><span class="line"><span class="cl"><span class="o">[</span>+<span class="o">]</span> Running 7/7
</span></span><span class="line"><span class="cl"> ⠿ Network demo_default                Created      0.0s
</span></span><span class="line"><span class="cl"> ⠿ Container prometheus                Started      0.6s
</span></span><span class="line"><span class="cl"> ⠿ Container demo-jaeger-all-in-one-1  Started      0.5s
</span></span><span class="line"><span class="cl"> ⠿ Container demo-zipkin-all-in-one-1  Started      0.4s
</span></span><span class="line"><span class="cl"> ⠿ Container demo-otel-collector-1     Started      0.7s
</span></span><span class="line"><span class="cl"> ⠿ Container demo-demo-server-1        Started      0.9s
</span></span><span class="line"><span class="cl"> ⠿ Container demo-demo-client-1        Started      1.1s
</span></span></code></pre></div><p>The collector runs on port 4317, so any other error message than a timeout/connection error, means that the application is running:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-shell" data-lang="shell"><span class="line"><span class="cl">$ curl localhost:4317
</span></span><span class="line"><span class="cl">curl: <span class="o">(</span>1<span class="o">)</span> Received HTTP/0.9 when not allowed
</span></span></code></pre></div><p>With this setup, in addition to the open telemetry collector, we also get <a href="https://github.com/jaegertracing/jaeger-ui">Jaeger UI</a> running under <a href="http://0.0.0.0:16686/">http://0.0.0.0:16686/</a> which will help in visualizing the calls.</p>
<p>The next step is to run the code with the auto-instrumentation agent. For this we need a few packages to be added as part of the project setup. In the post, I use poetry to manage python projects, but you can <a href="https://opentelemetry.io/docs/instrumentation/python/automatic/#setup">install the packages</a> with pip as well. First, we add the instrumentation for the Python requests library and the telemetry exporter.</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-shell" data-lang="shell"><span class="line"><span class="cl">$ poetry add opentelemetry-instrumentation-requests
</span></span><span class="line"><span class="cl">$ poetry add opentelemetry-exporter-otlp
</span></span></code></pre></div><p>Afterwards, we add the agent via:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-shell" data-lang="shell"><span class="line"><span class="cl">$ poetry add opentelemetry-distro
</span></span></code></pre></div><p>and start the application via the agent (<a href="https://opentelemetry.io/docs/instrumentation/python/automatic/#configuring-the-agent">see reference</a>) and keep a text logfile:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-shell" data-lang="shell"><span class="line"><span class="cl">$ poetry run opentelemetry-instrument --traces_exporter console,otlp <span class="se">\
</span></span></span><span class="line"><span class="cl">--metrics_exporter console <span class="se">\
</span></span></span><span class="line"><span class="cl">--service_name llm-playground <span class="se">\
</span></span></span><span class="line"><span class="cl">--exporter_otlp_endpoint 0.0.0.0:4317 <span class="se">\
</span></span></span><span class="line"><span class="cl">python main.py <span class="p">|</span> tee output.log
</span></span></code></pre></div><p>If the metrics collector on port 4317 is not running correctly, the app will log an error:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-shell" data-lang="shell"><span class="line"><span class="cl">WARNING:opentelemetry.exporter.otlp.proto.grpc.exporter:Transient error StatusCode.UNAVAILABLE encountered <span class="k">while</span> exporting traces, retrying in 1s.
</span></span></code></pre></div><p>In case of SSL handshake issues (or similar ones)</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-shell" data-lang="shell"><span class="line"><span class="cl">E0423 17:04:25.197068000 <span class="m">6150713344</span> ssl_transport_security.cc:1420<span class="o">]</span>    Handshake failed with fatal error SSL_ERROR_SSL: error:100000f7:SSL routines:OPENSSL_internal:WRONG_VERSION_NUMBER.
</span></span></code></pre></div><p>one can instruct the exporter with an <a href="https://github.com/open-telemetry/opentelemetry-specification/blob/773ee656f92c7f591f2fd9c38df82c264a15184d/specification/protocol/exporter.md?plain=1#L19-L21">environment variable</a> to ignore SSL errors:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-shell" data-lang="shell"><span class="line"><span class="cl">$ <span class="nb">export</span> <span class="nv">OTEL_EXPORTER_OTLP_INSECURE</span><span class="o">=</span><span class="nb">true</span>
</span></span></code></pre></div><p>If this does not help to establish connectivity, try <a href="https://opentelemetry.io/docs/instrumentation/python/automatic/#grpc-connectivity">increasing the verbosity of gRPC logging</a> to find the error.</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-shell" data-lang="shell"><span class="line"><span class="cl">$ <span class="nb">export</span> <span class="nv">GRPC_VERBOSITY</span><span class="o">=</span>debug
</span></span><span class="line"><span class="cl">$ <span class="nb">export</span> <span class="nv">GRPC_TRACE</span><span class="o">=</span>http,call_error,connectivity_state
</span></span></code></pre></div><p>As configured in “traces_exporter”, in addition to the OTLP endpoint, the spans are also written to the console. In the demo I’m running, the code uses <a href="https://gradio.app/">gradio</a> to create a simple UI. I was surprised to see four calls made by gradio even if <a href="https://gradio.app/docs/#interface-launch-header">launched</a> without any public sharing: <code>launch(share=False)</code>.</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">$ cat output.log <span class="p">|</span> grep http.url
</span></span><span class="line"><span class="cl"><span class="s2">&#34;http.url&#34;</span>: <span class="s2">&#34;&lt;https://checkip.amazonaws.com/&gt;&#34;</span>,
</span></span><span class="line"><span class="cl"><span class="s2">&#34;http.url&#34;</span>: <span class="s2">&#34;&lt;https://api.gradio.app/gradio-messaging/en&gt;&#34;</span>,
</span></span><span class="line"><span class="cl"><span class="s2">&#34;http.url&#34;</span>: <span class="s2">&#34;&lt;https://api.gradio.app/pkg-version&gt;&#34;</span>,
</span></span><span class="line"><span class="cl"><span class="s2">&#34;http.url&#34;</span>: <span class="s2">&#34;&lt;http://127.0.0.1:7860/startup-events&gt;&#34;</span>,
</span></span><span class="line"><span class="cl"><span class="s2">&#34;http.url&#34;</span>: <span class="s2">&#34;&lt;http://127.0.0.1:7860/&gt;&#34;</span>,
</span></span><span class="line"><span class="cl"><span class="s2">&#34;http.url&#34;</span>: <span class="s2">&#34;&lt;https://api.gradio.app/gradio-initiated-analytics/&gt;&#34;</span>,
</span></span><span class="line"><span class="cl"><span class="s2">&#34;http.url&#34;</span>: <span class="s2">&#34;&lt;https://api.gradio.app/gradio-launched-analytics/&gt;&#34;</span>,
</span></span><span class="line"><span class="cl"><span class="s2">&#34;http.url&#34;</span>: <span class="s2">&#34;&lt;https://api.gradio.app/gradio-launched-telemetry/&gt;&#34;</span>,
</span></span></code></pre></div><p><a href="https://gradio.app/docs/#interface-launch-header">According to the docs</a>, there is a parameter <code>analytics_enabled</code> and a <code>GRADIO_ANALYTICS_ENABLED</code> environment variable. The description is really convoluted though <em>“default: None; If None, will use environment variable or default to True”</em>&hellip;</p>
<figure class="align-center ">
    <img loading="lazy" src="1_2KEDWz8PDxjPDWeWWjkseA.png#center"
         alt="Gradio documentation screenshot for property “analytics_enabled”"/> <figcaption>
            <p>Gradio documentation screenshot for property “analytics_enabled”</p>
        </figcaption>
</figure>

<p>Back in the telemetry data, we also find the expected OpenAI requests:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="s2">&#34;http.url&#34;</span>: <span class="s2">&#34;&lt;https://api.openai.com/v1/engines/text-embedding-ada-002/embeddings&gt;&#34;</span>,
</span></span><span class="line"><span class="cl"><span class="s2">&#34;http.url&#34;</span>: <span class="s2">&#34;&lt;https://api.openai.com/v1/completions&gt;&#34;</span>,
</span></span></code></pre></div><p>The collected telemetry is helpful to understand the amount and latency of the requests over time. Let’s filter in Jaeger UI (<a href="http://0.0.0.0:16686/">http://0.0.0.0:16686/</a>) for one of the URLs using the tag <code>http.url=https://api.openai.com/v1/engines/text-embedding-ada-002/embeddings</code>:</p>
<figure class="align-center ">
    <img loading="lazy" src="1_G_uIu0dM4srjXpEfXCQp0w.png#center"
         alt="Jaeger UI showing traces for calls to OpenAI’s embedding URL endpoint"/> <figcaption>
            <p>Jaeger UI showing traces for calls to OpenAI’s embedding URL endpoint</p>
        </figcaption>
</figure>

<p>We see 20 calls in the last hour ranging from 280 to 883 ms. Each call can be expanded to check for a bit more details:</p>
<figure class="align-center ">
    <img loading="lazy" src="1_dootEEKfqFmi37aeWKnv4Q.png#center"
         alt="Jaeger UI showing details of a single UI call"/> <figcaption>
            <p>Jaeger UI showing details of a single UI call</p>
        </figcaption>
</figure>

<p>Sadly, the UIs of Jaeger and Zipkin are unexpectedly basic and don’t support wildcard searches, so one still needs to <code>tee</code> the console output to a file to quickly search for all the called APIs.</p>
<h2 id="summary">Summary</h2>
<p>For more rich telemetry information with custom metadata, the calls to OpenAI (and others) would need to be <a href="https://opentelemetry.io/docs/instrumentation/python/getting-started/#add-manual-instrumentation-to-automatic-instrumentation">instrumented manually</a>. While Langchain comes with <a href="https://python.langchain.com/en/latest/modules/callbacks/getting_started.html#callbacks">Callbacks</a> that provide access to the API call results, at time of writing LlamaIndex does not have a similar mechanism for <a href="https://github.com/jerryjliu/llama_index/blob/main/gpt_index/embeddings/openai.py">OpenAI embeddings</a>.</p>
<p>The automatic instrumentation is an easy way to inspect calls made by Python applications. The early-stage libraries built in the LLM context tend to have lots of defaults that magically make calls to OpenAI or call lots of 3rd party APIs and externalize its vector data. Even supposedly <a href="https://github.com/Helicone/helicone#example-env-file">open-source applications for self-hosting</a> proxies to OpenAI, still rely on other 3rd party services. OpenTelemetry provides an easy way to verify external calls in a sandbox environment.</p>
<p><em>If you’re interested in getting more out of OpenTelemetry, check out the follow-up post, that gets into details of <em><a href="https://ocytko.net/posts/opentelemetry-meets-openai-manual-instrumentation/"><em>manual instrumentation with OpenTelemetry for Langchain and LlamaIndex</em></a></em>.</em></p>
<h2 id="references">References</h2>
<ul>
<li><a href="https://opentelemetry.io/docs/instrumentation/python/automatic/">OpenTelemetry Python Automatic Instrumentation</a></li>
<li><a href="https://github.com/open-telemetry/opentelemetry-specification/blob/773ee656f92c7f591f2fd9c38df82c264a15184d/specification/protocol/exporter.md?plain=1#L19-L21">OpenTelemetry Exporter Configuration Options</a> (environment variables)</li>
<li><a href="https://beebom-com.cdn.ampproject.org/c/s/beebom.com/how-build-own-ai-chatbot-with-chatgpt-api/amp/">Building AI Chatbot with ChatGPT API</a> and <a href="https://gpt-index.readthedocs.io/en/latest/guides/building_a_chatbot.html">LlamaIndex: Building a Chatbot</a></li>
<li>Dependencies section for poetry:</li>
</ul>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt">1
</span><span class="lnt">2
</span><span class="lnt">3
</span><span class="lnt">4
</span><span class="lnt">5
</span><span class="lnt">6
</span><span class="lnt">7
</span><span class="lnt">8
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-ini" data-lang="ini"><span class="line"><span class="cl"><span class="k">[tool.poetry.dependencies]</span>
</span></span><span class="line"><span class="cl"><span class="na">python</span> <span class="o">=</span> <span class="s">&#34;^3.11&#34;</span>
</span></span><span class="line"><span class="cl"><span class="na">openai</span> <span class="o">=</span> <span class="s">&#34;^0.27.2&#34;</span>
</span></span><span class="line"><span class="cl"><span class="na">llama-index</span> <span class="o">=</span> <span class="s">&#34;^0.4.32&#34;</span>
</span></span><span class="line"><span class="cl"><span class="na">gradio</span> <span class="o">=</span> <span class="s">&#34;^3.22.1&#34;</span>
</span></span><span class="line"><span class="cl"><span class="na">opentelemetry-instrumentation-requests</span> <span class="o">=</span> <span class="s">&#34;^0.38b0&#34;</span>
</span></span><span class="line"><span class="cl"><span class="na">opentelemetry-distro</span> <span class="o">=</span> <span class="s">&#34;^0.38b0&#34;</span>
</span></span><span class="line"><span class="cl"><span class="na">opentelemetry-exporter-otlp</span> <span class="o">=</span> <span class="s">&#34;^1.17.0&#34;</span>
</span></span></code></pre></td></tr></table>
</div>
</div>]]></content:encoded>
    </item>
    <item>
      <title>Emergency Procedures in SRE</title>
      <link>https://ocytko.net/posts/emergency-procedures-in-sre/</link>
      <pubDate>Tue, 24 Jan 2023 19:00:12 +0000</pubDate>
      <guid>https://ocytko.net/posts/emergency-procedures-in-sre/</guid>
      <description>Emergency procedures aim at stabilizing the system in a degraded state. When used properly, they result in faster incident response and &amp;hellip;</description>
      <content:encoded><![CDATA[<p><em>Originally published on <a href="https://medium.com/@bocytko/emergency-procedures-in-sre-e3297f9add66">medium</a>.</em></p>
<p>Emergency procedures used in incident response are aimed at stabilizing the system in a degraded state. When used properly, they result in faster incident response and become a foundation for further resiliency improvements in your system. In the post we’ll also explore how emergency procedures differ from runbooks.</p>
<h2 id="runbooks">Runbooks</h2>
<p>Imagine you get paged in the middle of the night. The situation you encounter looks familiar to you and you’re sure you or your colleague had dealt with it before. However, as seconds ago your were still in deep sleep, you simply can’t recall what to do. Wouldn’t it be great if you had something to help your memory? This is what <a href="https://www.pagerduty.com/resources/learn/what-is-a-runbook/">runbooks</a> are for.</p>
<p>Typically linked in the description of the alert that paged you, runbooks describe routine procedures aimed at restoring regular service of your application. Runbooks list the exact steps to take. It’s helpful to name a runbook with a short summary of the executed procedure as title (e.g. “scale up application”, “retrigger batch job”). Other types of runbooks document migrations, failovers, DB upgrades to help retain know-how within your teams as this type of work is typically executed rather infrequently. Lastly, there are also runbooks aimed at helping with triaging unknown failure situations. When followed, they help inspect typical metrics of the system across layers and components in search for the culprit of the observed issues. A good example here is the sequence of commands used in Brendan Gregg’s <a href="https://www.brendangregg.com/blog/2015-12-03/linux-perf-60s-video.html">Linux performance analysis in 60 seconds</a>.</p>
<h2 id="emergency-procedures">Emergency procedures</h2>
<p>Let’s consider a different scenario: when paged, you observe that the system is overloaded and users experience increased latencies and error rates. You can’t scale up the system due to your dependencies (or storages) taking too long to scale up. To restore service you need to reduce load by 25% as soon as possible. How will you proceed? Will you disable feature A or B? Will you degrade service for user/country X or Y? Do you even have means to do so?</p>
<p>Enter emergency procedures. Unlike for typical runbooks, the goal of an emergency procedure is to bring the system into a degraded, yet stable state. Such state needs to be acceptable to users and stakeholders while trading off availability over customer experience.</p>
<h2 id="structure">Structure</h2>
<p>Emergency procedures have defined trigger conditions and impact, both from the business and operational side. It’s important that they’re agreed with business owners ahead of time and thus do not require active approval during the incident response. Impact can be expressed in customer behavior, description how a feature will be working when the procedure has been carried out, or expressed in the change to the system’s load, for example.</p>
<p>Here an example for a food delivery application:</p>
<blockquote>
<ul>
<li><strong>title</strong>: reduce search radius to 400m</li>
<li><strong>trigger</strong>: increased latency or error rate for search queries</li>
<li><strong>business impact</strong>: as all search queries will be limited to a max. 400m radius, customers will see 10–20% less search results, leading to a drop in conversion rate</li>
<li><strong>operational impact</strong>: load on the datastore load will be reduced by 20% within 2 minutes of activating the feature toggle</li>
<li><strong>steps</strong>: &hellip;</li>
</ul>
</blockquote>
<p>It’s important that the on-call team regularly practices the emergency procedures. This will verify the correctness of the to be executed steps and operational implications. Additionally, it ensures that the team (and stakeholders) are familiar with the degraded state of the system, which would be rarely observed otherwise. It’s highly recommended to include stakeholder contacts in the procedures in order to keep them informed about the interventions taken during incident response.</p>
<h2 id="designing-for-resilience">Designing for resilience</h2>
<p>Systems need to be explicitly designed for supporting emergency procedures. Be it through runtime toggles that allow controlling certain features (e.g. on/off switches, enabling less expensive processing using cached values), or infrastructure mechanisms (e.g. short-circuiting processing for certain user groups or request types). This also requires annotating incoming requests with sufficient metadata to be able to apply differing treatment per feature, traffic origin, etc.</p>
<p>Here a few example degradations that can be introduced to a system with the system property outlined in curly braces:</p>
<ul>
<li>enforce serving data from a cache instead to reduce load on the datastore (data freshness)</li>
<li>always serve the first page of a result set to reduce load on the datastore (data completeness)</li>
<li>limit retrieved records to N reducing the working dataset of the DB (data completeness defined by amount, distance, or time)</li>
<li>switch HD video to SD (degrade quality to save bandwidth) or serve images instead (reduce load by preventing auto-play)</li>
<li>drop traffic from unauthenticated users (user coverage)</li>
<li>pause all asynchronous batch jobs (feature completeness, data freshness)</li>
<li>process only critical requests (feature degradation, data completeness)</li>
</ul>
<h2 id="automation">Automation</h2>
<p>It’s certainly advisable to automate frequently used emergency procedures over time by building them into the system as part of your resiliency patterns (fallbacks, retries on error with adjusted input, etc.). Manual execution of the emergency procedure ensures that a human assesses the situation before proceeding with the procedure, which helps harden the defined preconditions. A few manual executions enable you to evaluate if automation is really of value when compared with than other product features planned. It’s important to factor in on-call health into the prioritization. At times of <a href="https://leaddev.com/leaddev-live/scaling-incident-management-how-we-grew-google-meet-50x-during-covid19">unexpectedly high growth</a> when system availability is a concern, automation is just necessary to cope with overload scenarios efficiently.</p>
<h2 id="conclusion">Conclusion</h2>
<p>Defining emergency procedures requires taking a different view on your system — one where some features are explicitly switched into a degraded mode, thus enabling the overall system to get healthy. The thought exercise of imagining such a degraded, yet usable state is time well spent and highly recommended throughout the design or production readiness stage for your applications. By building in the necessary failure handling mechanisms into the software, or designing it in a way that naturally accommodates the failure states, incident mitigation becomes simpler and less tedious. Having the procedures at hand, you will thank yourself next time you’re on-call in the middle of the night (or day).</p>
]]></content:encoded>
    </item>
    <item>
      <title>Building and shipping in iterations</title>
      <link>https://ocytko.net/posts/building-and-shipping-in-iterations/</link>
      <pubDate>Wed, 02 Nov 2022 07:56:10 +0000</pubDate>
      <guid>https://ocytko.net/posts/building-and-shipping-in-iterations/</guid>
      <description>This posts showcases strategies for shipping software in iterations, inspired by TDD, Coding Katas, and Testing in Production.</description>
      <content:encoded><![CDATA[<p><em>Originally published on <a href="https://medium.com/@bocytko/building-and-shipping-in-iterations-366e8359cac8">medium</a>.</em></p>
<p>The <a href="https://www.coderetreat.org/">Global Day of Code Retreat</a> is coming soon. Going down the memory lane of Coding Katas, Test-Driven-Development and the related workshops I had the opportunity to attend/host, I got reminded of the long-term value of these exercises.</p>
<p>This posts showcases strategies for shipping software in iterations, which build on the fundamental practices introduced in these workshops. Just like the small steps in coding katas, the strategies outlined here may seem obvious or too simple to leverage in a real-life project, until one encounters the right project to apply them in.</p>
<h2 id="introduction">Introduction</h2>
<h3 id="code-retreat-workshops"><strong>Code Retreat workshops</strong></h3>
<p>The Code Retreat format promotes Test-Driven-Development through coding katas and injects added fun through pairing, learning from one another, and making the exercises harder through constraints (e.g. no loops). While Katas may feel simple and a repetitive exercise helping getting into flow or learning a new programming language/construct, there is an implicit concept they teach us. It’s all about the simple iterations and sequences of tests that one defines in order to complete the set of requirements in an iterative way. The important bit is that the complexity level may only increase in a gradual way. In the meantime, this concept has been codified as <a href="https://en.wikipedia.org/wiki/Transformation_Priority_Premise">Transformation Priority Premises</a>, which to the familiar eye will look like an extension of the intuitive evolution of null → single result → two results → loop, etc.</p>
<h3 id="elephant-carpaccio"><strong>Elephant Carpaccio</strong></h3>
<p>The <a href="https://alistaircockburn.com/Elephant-Carpaccio">Elephant Carpaccio</a> exercise takes the concept of developing in iterations to the next level. Its aim is to teach how to slice a feature into very small iterations spanning all layers. One is asked to define a detailed iteration plan and set of acceptance criteria for each increment. The caveat is that each iteration is to be delivered in minutes! (without Copilot / GPT-3 support ;-)) While tricky at first, with some practice during the workshop this gets rather fun quickly. <a href="https://docs.google.com/document/d/1TCuuu-8Mm14oxsOnlk8DqfZAA1cvtYu9WGv67Yj_sSk/pub">Try it</a> in your team if you haven’t yet!</p>
<h2 id="ship-early-and-ship-often-singlefeature">Ship early and ship often! (single feature)</h2>
<p>More often than not, developers have the tendency to ship features only when fully ready, delaying integration and increasing pull request size (and as a result the lead time). For the team, it’s hard to understand the quality and progress on the feature as the code isn’t used in production and never deployed. As most problems happen in production, this doesn’t sound like a good strategy, right?</p>
<p>A common technique in teams practicing trunk-based development is to hide the feature behind a feature flag. This allows for frequent code integration and avoids unintended production usage. It’s a step forward, but heavily missing out on the value the code may have from actually being used in production continuously, even when the feature is not fully finished (or not even a single line of code of logic is written).</p>
<h2 id="testing-latency-assumptions">Testing latency assumptions</h2>
<p>Whenever new service calls or complex calculations are are added to an application, the overall response time of the application may increase. Typically, services will have a defined latency SLO for its operations, which clients rely on and use to define timeouts. In other cases, increased response time will have immediate business implications as users will interact with the software less frequently as it gets slower or abandon the application along the journey. It’s possible to use A/B experiments to validate the latency buffer a service has, but these may take weeks to reach statistical significance. Hence, there is value in learning early in a project that the pure added latency triggers undesired business impact for users.</p>
<p>Instead of waiting for the feature to be fully implemented, shipping code that’s a no-op in terms of business-logic, but such that simulates the processing time allows to verify latency impact of the new functions. When code runs in production with the added latency, impact on processes, end-user KPIs will become immediately visible, which helps to validate that the chosen design approach is viable in production early in the development cycle. As clients spend more CPU time to process the responses, this also provides early insights for capacity planning.</p>
<p>To prepare for adding business logic, we add an execution budget after which the function terminates automatically and returns a fallback value. Lastly, business logic can be added in multiple iterations, relying on the guaranteed execution time to cover for performance inefficiencies. At this last step, the added latency is optional and can be dropped as soon as the function returns values for all input combinations.</p>
<p>The diagram below demonstrates the iterative evolution of the function code added to an example application. Each iteration ends with a production deployment. The 4th step should have additional iterations to develop the business logic.</p>
<figure class="align-center ">
    <img loading="lazy" src="1_ZxoY8CWK0XQmrpfwICxDww.png#center"
         alt="A diagram visualizing the iterative evolution of function code, starting with a simple no-op, through adding latency that’s later incrementally replaced with business logic."/> <figcaption>
            <p>Iterative evolution of function code</p>
        </figcaption>
</figure>

<h2 id="verifying-inputs-and-discovering-edgecases">Verifying inputs and discovering edge cases</h2>
<p>When faced with business logic needing to process many input parameters, it’s helpful to learn more about the input parameters and data access patterns. Frequently, this task cannot be done based on data dumps.</p>
<p>Shipping code that just processes the input parameters allows to calculate the distribution of input parameters, estimate the likelihood of certain edge cases to occur in production (and these tend to occur more frequently than expected), or to record the access patterns based on combination of inputs or frequency of use. This helps in selecting the right data structures for efficient processing, define strategies for populating caches, etc. Long-term, the already developed input verification procedures can help detect data skew or inform that new use cases were added to production, which may invalidate assumptions taken during development.</p>
<p>The function can either store the statistics for requests in memory and/or log these with every n-th call. To minimize impact on the main execution flow of the application, incoming traffic can be duplicated into a separate application that logs required statistics over time.</p>
<figure class="align-center ">
    <img loading="lazy" src="1_vU6MbP7GzOthCqdJkEj-LQ.png#center"
         alt="A diagram visualizing the concept of early data verification through calculating statistics on the input data and emitting these via logs."/> <figcaption>
            <p>Verifying input data to a function early in development</p>
        </figcaption>
</figure>

<h2 id="shipping-logic-in-iterations">Shipping logic in iterations</h2>
<p>Just like in TDD where the passing (acceptance) tests demonstrate real progress, the iterations of the code you shipped showcase progress that’s verified in production. This allows to understand when a certain iteration triggers problems in production. As soon as first use cases are ready for production use, the service can invoke the logic for these asynchronously and log results for offline verification.</p>
<p>For simple cases, it’s sufficient to log the input and output to perform offline verification of the results. For complex cases, like rewrites or migrations (see picture below) it’s advised to record the result of both operations and to perform additional asynchronous result comparison, helping to measure and log the correctness of the performed operations (often referred to as the <a href="https://engineering.zalando.com/posts/2021/11/parallel-run.html">Parallel Run Pattern</a>). These comparisons also showcase the incremental progress of the development work, which is much needed in migrations.</p>
<figure class="align-center ">
    <img loading="lazy" src="1_Z4EPonDveJTfD7MCckcamw.png#center"
         alt="A diagram showing how two implementations of a function are invoked in parallel in order to compare results and measure migration progress/completeness."/> <figcaption>
            <p>Invoking two implementations in parallel for result comparison</p>
        </figcaption>
</figure>

<p>Given sufficient confidence for a subset of use cases, the source of truth of the operation can be shifted to the new implementation, which is the essence of the <a href="https://martinfowler.com/bliki/StranglerFigApplication.html">Strangler Fig Pattern</a>.</p>
<h2 id="being-responsible">Being responsible!</h2>
<p>Production deployments of the described patterns require high discipline. For high traffic use cases, it is advised to invoke early stage code infrequently, e.g. for a fraction of incoming requests or through capping the number of executions per time unit. The mentioned technique of leveraging latency budgets by ensuring maximum execution time of the executed code helps in ensuring that early stage code has predictable runtime implications. Impact on memory footprint needs to be managed carefully as well. This is easy to achieve through limiting the size of any intermediate results or statistics computed, stored, and logged. Unsurprisingly, these are practices that come in handy nonetheless and would be built up eventually over time, often as a result of production hardening or (worst case) in response to incidents.</p>
<h2 id="untangling-dependencies-cross-team-projects">Untangling dependencies! (cross-team projects)</h2>
<p>The strategies described so far apply mostly to single functions or applications. In cross-team projects, dependencies are typically the limiting factor for successful project delivery on time. Dependencies in larger projects cannot be avoided and need to be managed. Most likely, you will be familiar with one of the following situations:</p>
<ul>
<li>Frontend teams waiting for a backend API to be ready, so that they can start building UI widgets and wiring it to the provided data.</li>
<li>Backend teams waiting for dependencies on APIs providing the data/fields they need to calculate results.</li>
<li>Analysts delaying building dashboards until the time when data is available in production. Unless, built-in in the UI frameworks, interaction data tends to be de-scoped until the very end.</li>
<li>Teams waiting for UI components to be ready and shown in production, so that they can verify the end-to-end user journey by clicking a button to initiate an action as part of their tests.</li>
</ul>
<p>The waiting time in each of the scenarios is the factor to focus on when improving project delivery speed. Here, lessons from the Elephant Carpaccio exercise come in handy and trigger thinking in end-to-end iterations. Coupled with addressing identified dependencies early on in the project it’s a powerful combo.</p>
<h2 id="shipping-the-simplestslice">Shipping the simplest slice</h2>
<p>The more dependencies, the more it’s important to manage scope and optimize for early integration. Just like the slices defined in the exercise, the value of small iterations shipped to production cannot be understated. More often than not, simplest will be counter-intuitive to what one is used to.</p>
<p>Building a complex UI widget with a CTA button? From a dependency point of view, it’s the button that really matters as it links data with the destination. Shipping a UI component with just the button unblocks teams building and testing the triggered action along with providing the click events/data. When looked at visually, the widget may feel odd as it would not be shown to your users, but reducing dependencies enables the team to focus on further iterations of the UI component itself. It’s also a great starting point to reserve screen estate for the full component and observe its incremental evolution. Naturally, such change requires to be hidden behind a feature toggle, so that this button is shown only when desired during testing.</p>
<h2 id="api-first">API first</h2>
<p>While widely practiced through IDLs or OpenAPI specifications that help auto-generate API clients, just the specs may not be enough. To untangle dependencies, beyond agreement on the API contracts, it’s essential to provide a set of sample responses when the code is invoked in production. Such responses can be then returned by API endpoints serving static responses, alternating through sample requests or switching responses based on input context. In case a new service needs to be built, a simple web server serving static files may be enough to start with (even if later replaced by a different application). Re-using any contract tests is helpful here as well, as long as the responses are used in early stage development, in production.</p>
<h2 id="early-end-to-end-integration">Early end-to-end integration</h2>
<p>The earlier features built by multiple teams are integrated with one another, the better. It’s helpful to align on a small end-to-end deliverable across all teams early in the project. In this iteration, API endpoints with sample responses and clients for those APIs are developed to invoke APIs and process (at least) a few API parameters (it’s worth noting that events are also APIs).</p>
<p>When the integration in production succeeds, it ensures that every piece of the puzzle can be further iterated on with relaxed dependencies. Early deployment to production enables everyone to observe incremental progress, incl. interaction events in analytics systems. Moreover, when the features are treated as production-grade from the start, the team continuously learns about daily operations of the system.</p>
<p>Lastly, through simulation of system (end-user) activity, non-functional requirements can be verified uncovering performance bottlenecks. Applications generating such simulated traffic (be it UI interactions, API calls, or events pushed into the system) can be leveraged as part of CI/CD pipelines as smoke tests executed ahead of releasing a change. Additionally, such simulator can serve as an end-to-end probe continuously verifying that the system works in production.</p>
<figure class="align-center ">
    <img loading="lazy" src="1_zB5gRMDayobo9mDUayFuhQ.png#center"
         alt="A diagram showing an application with two synchronous dependencies and a simulator application that simulates input events, API calls and verifies the results/events of these operations."/> <figcaption>
            <p>Input data simulation and result verification</p>
        </figcaption>
</figure>

<h2 id="summary">Summary</h2>
<p>The strategies outlined in this post can help in reducing risk of software delivery by shipping software to production early, in small, and well-defined iterations. Applied to the right problem, they will result in more frequent code integration and deployment to production. Teams are enabled to gather information about the complexity of the problem at hand at the design stage of the feature delivery, untangle dependencies, and provide more transparency on the progress of the project.</p>
<p>Shipping to production should be frequent and fun! If it’s not, do it more often and address the factors leading to toil or anxiety, be it through automation of lengthy and manual procedures, by adding gradual deployments with automatic rollbacks, or canary builds to reduce the blast radius of failures. In cloud-native environments, there are plenty of tools making it easy to adopt these deployment practices.</p>
]]></content:encoded>
    </item>
    <item>
      <title>Accepting and supporting changes</title>
      <link>https://ocytko.net/posts/accepting-and-supporting-changes/</link>
      <pubDate>Sun, 10 Jul 2022 14:40:56 +0000</pubDate>
      <guid>https://ocytko.net/posts/accepting-and-supporting-changes/</guid>
      <description>Explores how individuals can react to changes and focus their efforts to support the change to foster their own development.</description>
      <content:encoded><![CDATA[<p><em>Originally published on <a href="https://medium.com/@bocytko/accepting-and-supporting-changes-76c0c52cc501">medium</a>.</em></p>
<p>My <a href="https://ocytko.net/posts/how-to-communicate-major-changes/">previous post</a> outlined three key elements of successfully preparing and announcing changes in organizations from the perspective of the change author and driver. This post looks at change from the perspective of individuals on the receiving side of the change process — either those affected by the change or those who need to communicate the change to others, often while not having worked on the changes themselves. It explores how individuals can react to changes and focus their efforts and energy to support the change to the benefit of the organization and to foster their own development.</p>
<h2 id="accepting-thechange">Accepting the change</h2>
<p>Some changes are all but straightforward and will affect individuals, for example by changing how they work through rules, policies, or restructuring the organization. Regardless how well prepared, changes will lead to an emotional response and initial resistance. The worst thing you can do is to act immediately and succumb to emotions. The goal is to channel your energy into actions yielding material results. More often than not, you will regret critiquing, rejecting, or questioning change authors before understanding the full extent of the changes and its implications.</p>
<p>While it may not be easy, as first step you need to work towards accepting the change, thus building a foundation for further action. To help you gain some distance, let the change sink in and postpone your reaction to the next day. Afterwards, ensure you understand the communicated change and rationale. Helpful questions for your leader or the person communicating the change may include:</p>
<ul>
<li>Any clarification question to the contents of the message.</li>
<li>What was the trigger for the change? What external factors were at play?</li>
<li>How will the change benefit the team/company?</li>
<li>How is my role affected? How am I personally affected?</li>
<li>From when on are the changes going to be effective from?</li>
<li>How can feedback be provided?</li>
</ul>
<p>Noting down the assumptions you had when being presented with the change and contrasting these with the answers you receive, helps you compare your viewpoints. Having a full picture of differing viewpoints will help you in processing the change.</p>
<p>It’s important to note that as a manager and thus member of the leadership team, you are expected to accept the change by default. In cases where you have not worked on the change directly, this may be a more difficult ask. Your task is to discuss any of your concerns and questions with your manager and later commit to the changes and communicate these to others and your team as if you would have worked on these directly.</p>
<h2 id="providing-feedback">Providing feedback</h2>
<p>When processing the change, a helpful strategy is to formulate and provide feedback. Change authors will appreciate a validation of their assumptions and whether the key points from their communication were received and understood. Further, they may connect you with ongoing efforts supporting the rollout of the change.</p>
<p>You can help by collecting feedback from your peers and sharing it in the form of a summary document. By getting support from others, you reduce the pressure on yourself as you’re not the sole author of the feedback anymore. You also validate your understanding of the changes, though it’s important to be aware of <a href="https://en.wikipedia.org/wiki/Confirmation_bias">confirmation bias</a> here.</p>
<p>When formulating constructive feedback, you can follow a simple structure. First, highlight what you are addressing (e.g. communication cascade, motivation, measures) and explain how this matches/differs from your viewpoint/experience or point out aspects that potentially were not considered. Finally, express the outcome this has for you or others. Optionally, you can include a suggestion on what can help moving forward. For example: <em>“(…) The change was communicated mainly addressing X, leaving out Y who account for z% of the team. This decreased the mood among Y and made them feel excluded. Moving forward, would it be possible to ensure that Y is included explicitly in any follow-up communication?”</em></p>
<h2 id="supporting-thechange">Supporting the change</h2>
<p>The next step you can take after internalizing the change and discussing your feedback is participation in the change process. The better you understand (and believe in) the change, the more natural it will be for you to support the change. A good approach to start is by listening to your peers and getting insights on how they perceive the change. You may provide value in helping them understand the change, motivation, or implications of the change. Outlining how you perceive the change and what you see as an opportunity moving forward, will provide them with a different perspective, which they can relate to. You can help your peers to be helpful by outlining where they can support in the change implementation. Getting them involved will increase the buy-in and reduce anxiety for change through active participation.</p>
<p>Here a few practices that may help you in this task:</p>
<ul>
<li>Collecting feedback from peers along with suggestions on how to increase understanding and foster buy-in for the strategy.</li>
<li>Hosting a discussion within the team to form a mutual understanding of the change.</li>
<li>Volunteering for measures that are accompanying the change rollout.</li>
<li>Voicing support for the change and showcasing the opportunities it creates for the individuals, team, or organization.</li>
</ul>
<h2 id="supporting-changes-you-wish-neverhappened">Supporting changes you wish never happened</h2>
<p>Surely, you will encounter changes that you are not fond of or such that aren’t in line with your values or beliefs. There is little you can influence when things are decided and already in motion, so be aware of wasting your own energy here.</p>
<p>Instead, you can choose to be forward looking and get involved in future changes of similar type with the aim to correct mistakes that you think have been made. Alternatively, choosing to accept the status quo, you should ask yourself how the you can leverage the changed environment to your benefit. Explore how the changes affect your opportunities. What are things that will be easier? What are things that will not be required anymore? Are there new stakeholders or colleagues you can network with, which may help you achieve your targets?</p>
<h2 id="next-step-drivingchange">Next step: driving change</h2>
<p>Having supported a change and participated in its implementation gives you good foundations for initiating your own changes. It’s much more fun and impactful to work on a topic that is dear to one’s heart and <a href="https://www.rebelsatwork.com/resources">build one’s own rebel alliance</a> to initiate and lead the change. See my other posts <a href="https://ocytko.net/posts/driving-change-why-are-your-ideas-being-rejected/">Driving Change</a> for tips on initiating changes and <a href="https://ocytko.net/posts/how-to-communicate-major-changes/">How to communicate major changes</a> for advice on change rollout.</p>
]]></content:encoded>
    </item>
    <item>
      <title>How to communicate major changes?</title>
      <link>https://ocytko.net/posts/how-to-communicate-major-changes/</link>
      <pubDate>Sat, 19 Mar 2022 20:05:00 +0000</pubDate>
      <guid>https://ocytko.net/posts/how-to-communicate-major-changes/</guid>
      <description>This post focuses on 3 key elements of successfully preparing and announcing changes from the perspective of the change author and driver…</description>
      <content:encoded><![CDATA[<p><em>Originally published on <a href="https://medium.com/@bocytko/how-to-communicate-major-changes-1728680e84cd">medium</a>.</em></p>
<p>My <a href="https://ocytko.net/posts/driving-change-why-are-your-ideas-being-rejected/">previous post</a> described challenges with triggering changes bottom-up. This post focuses on three key elements of successfully preparing and announcing changes in organizations from the perspective of the change author and driver. The approach described here is mostly relevant for organizational or process changes.</p>
<p>The difficulty in announcing changes depends on the scope of change, the situation of the affected individuals, and the perceived difference compared to status quo. Diligent preparation and rollout planning for changes is crucial for driving a successful change process. Missing out on planning may lead to diminishing returns, create unnecessary friction, and put additional pressure on the change authors.</p>
<p>When preparing a change, three elements are crucial to consider:</p>
<ul>
<li>Set of key changes alongside with reasoning on why these were decided upon and which constraints were taken into account.</li>
<li>Change map with measures and messages appealing to the needs of those affected by the change and those who will support it.</li>
<li>Communication approach: announcement vs. communication cascade.</li>
</ul>
<p>Noting these down serves as an easy way to assess the confidence of change authors in the change and uncover areas that may have been missed out during preparation.</p>
<h2 id="key-changes-reasoning-and-constraints">Key changes, reasoning, and constraints</h2>
<p>Listing the key changes that are being made and preparing a crisp reasoning for each point helps in verifying the completeness of the scope of change. Capturing the constraints that influenced scope or decisions allows to set the right context as part of the announcement. It also ensures that assumptions about the status quo are validated ahead of proceeding further.</p>
<h2 id="the-change-map-four-quadrants-ofchange">The change map (four quadrants of change)</h2>
<p>Every person who is affected by a change is asked to adjust their behavior, which involves learning. Adults learn both on an individual level or collectively with/from their peers. Both ways have an internal and external dimension to it, forming a <a href="https://www.davittcorporatepartners.com/the-4-quadrants-of-change/">change map</a> or <a href="https://www.evolvingleaders.com.au/integral-model/">four quadrants of change</a>. The likelihood of success when rolling out changes is increased when the messages used throughout the communication as well as the additional measures accompanying the change process cover all of the four quadrants:</p>
<ul>
<li><strong>mindset/values</strong> (internal / individual) — What facts need to be acknowledged about the status quo, values, or beliefs of the individual to ensure the change is easy to understand and relate to? What measures are needed to promote buy-in? What key concerns of individuals need to be addressed in order to explain the reasons for the change, speak to their potential anxiety for change or fear of losing something they had or mastered before?</li>
<li><strong>behaviors+skills</strong> (external / individual) — how are individuals expected to act after the change is announced? What material can they read or interact with in order to learn about the changes? What skills to they need to acquire and what training opportunities are provided? What behaviours would define a successful rollout of the change? Which behaviours should be reinforced and which rather prevented from happening and how?</li>
<li><strong>cultural</strong> (internal / collective) — what support will be provided through changes in culture (or expectations), communities, and means of exchange for those who are affected by the change? How can groups be nudged or empowered to interact and discuss their concerns or understanding of the changes? Who will be the change agents and supporters/promoters?</li>
<li><strong>systems/processes</strong> (external / collective) — what processes or mechanisms will be put in place in order to support the change? Is there a need or opportunity for enforcing the change through process or rule changes? Will there be any mandatory trainings or re-skilling?</li>
</ul>
<p>Filling out the four dimensions during change preparation ensures coverage of the full spectrum of needs of the target audience (usually the internal dimensions) and organization (usually represented in the external dimensions). A common pitfall is to underinvest in the two quadrants with internal focus on the individual, resulting in merely stating the change and facts. This happens especially if there is external or time pressure or when the authors exhibit excitement and relief for the changes being finally ready to announce. The mindset/values quadrant ensures that care is given to help understanding the state of mind, knowledge, or needs of the target audience. It’s crucial to meet the audience where they are in order to convey a convincing and relatable message, thus reducing friction when the change is announced. Change authors need to be careful to keep a realistic view of the state of knowledge and sentiment within the organization. The longer the preparations take, the more familiar with and convinced they become of the new course, making it rather easy to underestimate the degree of change.</p>
<h2 id="communication-approach">Communication approach</h2>
<h3 id="announcement"><strong>Announcement</strong></h3>
<p>The simplest way to roll out a change is to announce it to everyone stating the facts (key changes), either in person or in writing. The bigger and more diverse the audience, the more care is required alongside the announcement text, with aspects prepared in the four quadrants required to be mentioned to increase success.</p>
<p>Regardless how sophisticated and clear the communication will be, there will (almost) always be questions. Including a short FAQ section in the change announcement helps to address anticipated questions upfront. To source these questions, put yourself in the position of the audience and those individuals who are most affected by the change. Try to anticipate what the most controversial or just the obvious reactions will be. To strengthen the set of questions, feedback can be sourced from a few individuals who have not worked on the change or from representatives of the leadership team.</p>
<p>To better structure the conversation around the change, it’s advisable to host a Q&amp;A session and to include an invite in the original announcement. Questions asked during the Q&amp;A should be also added to the FAQ later on.</p>
<h3 id="communication-cascade"><strong>Communication cascade</strong></h3>
<p>It’s common to combine the announcement with the cascade for major or complex changes. There are two types of cascaded rollouts, which differ in the role leadership plays in the change process — a supporting or leading one.</p>
<p>In the first model, the change initiators use dedicated sessions to announce the change to different groups, for example following the organizational hierarchy (following the reporting chain) or its structure (sessions per department). Here, the change authors play a stronger role in the rollout and are supported by leadership.</p>
<p>In the second model, the change is rolled out top-down with leaders owning the change, starting with the first leadership level who then roll it out to their team who communicate the change to the next level, and so on. The FAQ section mentioned above is extended at each level and leaders are supported with additional talking points which help to ensure consistent messaging. The key changes, reasoning, and constraints captured before serve as a base for the talking points and enable leaders to communicate the change as if they had worked on it themselves. Here, it’s also possible to adjust the messaging at every leadership level.</p>
<h2 id="summary">Summary</h2>
<p>Depending on the complexity of the change and size of the organization, sufficient time needs to be invested in preparation. How much time to invest is highly contextual and depends on the culture of the organization and the affected individuals. For simple changes, quickly noting the key changes and mapping the key messages onto the four quadrants will help you to tune the content of the change announcement. For complex changes, separating work on the three elements more explicitly is advised. The more changes you will initiate and drive, the better you will be prepared for the next one.</p>
<p>Should you encounter individuals challenging the changes, help them understand what <a href="https://ocytko.net/posts/accepting-and-supporting-changes/">opportunities they have to influence changes</a> and participate in the change process themselves.</p>
]]></content:encoded>
    </item>
    <item>
      <title>Most common design issues found during Production Readiness and Post-Incident Reviews</title>
      <link>https://ocytko.net/posts/most-common-design-issues-found-during-production-readiness-and-post-incident-reviews/</link>
      <pubDate>Sun, 24 May 2020 14:31:56 +0000</pubDate>
      <guid>https://ocytko.net/posts/most-common-design-issues-found-during-production-readiness-and-post-incident-reviews/</guid>
      <description>Operating software in production offers great insights into software quality. Learning from production incidents is key to improving…</description>
      <content:encoded><![CDATA[<p><em>Originally published on <a href="https://medium.com/@bocytko/most-common-design-issues-found-during-production-readiness-and-post-incident-reviews-47b2c9e14a9d">medium</a>.</em></p>
<p>Operating software in production offers great insights into software quality. Learning from production incidents is key to improving existing software and learning how to design reliable applications. The <a href="https://landing.google.com/sre/sre-book/chapters/evolving-sre-engagement-model/">Production Readiness Review</a> is an established practice in Site Reliability Engineering aiming at applying past post-incident experience and findings into the software development process. This post provides an overview of a few common themes and pitfalls I’ve experienced being surfaced during production readiness and post-incident reviews.</p>
<h2 id="using-defaults-is-just-asking-fortrouble">Using defaults is just asking for trouble</h2>
<p>Every framework, http client library or server, connection pool, database, and operating system assume defaults for configuration settings. Aside from <a href="https://blog.shodan.io/its-the-data-stupid/">risking publicly exposing sensitive data</a>, default settings are often impacting the application’s performance and reliability. There are important settings that must be revisited before deploying an application into a production environment.</p>
<p>Overall, the most commonly missed defaults that impact reliability are timeouts: <em>http client timeouts</em> (connection, read timeout), <em>DNS cache timeouts</em>, and database <em>connection pool and statement timeouts</em>. Framework authors bear great responsibility when setting default values, but often fail to make the developer’s life easy. For example, Java’s DNS cache <a href="https://stackoverflow.com/questions/1256556/how-to-make-java-honor-the-dns-caching-timeout#comment37330654_15282042">can be infinite</a>, Apache HttpClient’s v4 <a href="https://github.com/apache/httpcomponents-client/blob/62f2164b8939accfbec01cd4f923cf2202916fa4/httpclient/src/main/java/org/apache/http/client/config/RequestConfig.java#L270-L303">RequestConfig.Builder uses “-1” as default timeouts</a> with its documentation stating “<em>A negative value is interpreted as undefined (system default)</em>” and the newer v5 <a href="https://github.com/apache/httpcomponents-client/blob/92100e13a6aeb9ce04d23df2561c01b330cb961a/httpclient5/src/main/java/org/apache/hc/client5/http/config/RequestConfig.java#L45-L47">uses 3 minutes</a> instead, whereas .NET’s default timeout is <a href="https://docs.microsoft.com/en-us/dotnet/api/system.net.http.httpclient.timeout?view=netcore-3.1">100 seconds</a>. Timeouts need to be set with care, so it’s important to understand the meaning of the different configuration possibilities. For a great overview on the http request lifecycle and timeouts, check out <a href="https://blog.cloudflare.com/the-complete-guide-to-golang-net-http-timeouts/">guide to Go net/http timeouts</a>.</p>
<h2 id="misconfigured-reliability-patterns">Misconfigured reliability patterns</h2>
<h3 id="retries">Retries</h3>
<p><a href="https://landing.google.com/sre/sre-book/chapters/addressing-cascading-failures/#retries">Retrying</a> a failed http request is one of the simplest reliability patterns to implement. When done right, retries have an exponentially increasing wait time between attempts and <a href="https://aws.amazon.com/blogs/architecture/exponential-backoff-and-jitter/">leverage jitter</a> to prevent retry storms leading to the <a href="https://en.wikipedia.org/wiki/Thundering_herd_problem">thundering herd problem</a>. However, setting the timeouts to lower values than those that the dependency uses internally, will lead to subsequent retries that pile up and overload the dependency with work that is useless (given that the client won’t wait for the computation result it initially requested). Timeout values must therefore be carefully aligned with service providers, ideally based on their <a href="https://landing.google.com/sre/sre-book/chapters/service-level-objectives/">SLOs</a>.</p>
<h3 id="circuit-breakers">Circuit breakers</h3>
<p><a href="https://www.martinfowler.com/bliki/CircuitBreaker.html">Circuit breakers</a> enable the application to fail early in case of an overloaded or faulty dependency and serve a degraded experience via fallbacks. Dropping requests that would fail to be processed before its clients time out is also helping the dependency to recover from failure due to the load reduction. The additional time can be used by the service provider to stabilise the system (e.g. through provisioning of additional instances) and thus regain the ability to serve the required load.</p>
<p>Like every reliability pattern, it needs to be properly configured to function well. Configuring an execution timeout that is too high, will lead to a situation where the circuit breaker never opens, thus keeping the load on the dependency when its performance degrades and making recovery more difficult. Correctly configuring the circuit breaker requires careful planning <a href="https://github.com/Netflix/Hystrix/wiki/Configuration#threadpool-properties">based on peak load and p99 latencies</a>. The <a href="https://github.com/Netflix/Hystrix/wiki/Configuration">original Hystrix documentation</a> contains detailed guidance for this. Note that the default execution timeout is <em>1 second</em>. Too bad that official tutorials for frameworks (<a href="https://spring.io/guides/gs/circuit-breaker/">e.g. Spring</a>) fail to even mention the word “timeout” and do not link to the appropriate documentation.</p>
<h3 id="circuit-breakers-require-proper-isolation">Circuit breakers require proper isolation</h3>
<p>Even if timeouts are configured properly, the circuit breaker may not function as intended keeping the business use case in mind as the degradation will be too broad. Imagine an application <strong>A</strong> that is fetching a risk score for shipping addresses by calling system <strong>B</strong>. Because countries may be served by a different risk scoring provider, <strong>B</strong> will have multiple connectors (one for each provider) and will internally hold a logic defining which provider to choose based on the received address. Service <strong>A</strong> has a circuit breaker for calls to <strong>B</strong>. The failure rate of <strong>B</strong> however, will depend on the failure rate of the connected providers and the distribution of the requests across the providers. In such situation, a failure of a single provider can cause the circuit breaker to open preventing calls to be routed to the remaining providers thus degrading responses for all calls.</p>
<p>In the example below, <strong>B</strong> receives 300 rps and calls <strong>Provider 1</strong> with 200 rps. When <strong>Provider 1</strong> becomes unavailable, more than 50% of requests from <strong>A</strong> to <strong>B</strong> fail, causing the circuit breaker to open (following the <a href="https://github.com/Netflix/Hystrix/wiki/Configuration#circuitBreaker.errorThresholdPercentage">default configuration</a> of the popular Hystrix library) whereas requests routed to other providers would have been processed correctly.</p>
<figure class="align-center ">
    <img loading="lazy" src="1_9peO2sWzzWCC0qqNnqRwfw.png#center"
         alt="Figure 1. Provider 1 becomes unavailable, triggering the circuit breaker for calls from A to B to open and reject requests to healthy Providers 2 … N."/> <figcaption>
            <p>Figure 1. <strong>Provider 1</strong> becomes unavailable, triggering the circuit breaker for calls from <strong>A</strong> to <strong>B</strong> to open and reject requests to healthy <strong>Providers 2 … N</strong>.</p>
        </figcaption>
</figure>

<p>A potential solution for this type of insufficient isolation requires a custom strategy for counting the failure rate per provider or the creation of distinct circuit breakers per country within service <strong>A</strong>. Both solutions provide different types of isolation. Note that the former requires exposing additional information through the APIs (provider) whereas the latter is purely steered through knowledge that the caller already has based on the processed addresses (country). The right question to ask about this example is — why doesn’t <strong>B</strong> have circuit breakers to the individual providers? While these would be great to have, it’s often practically impossible, because <strong>B</strong> is a black box (e.g. a 3rd party service, a monolith that’s hard to adjust, …) and cannot be easily adjusted.</p>
<h2 id="mixing-synchronous-and-asynchronous-workloads">Mixing synchronous and asynchronous workloads</h2>
<p>Let’s imagine a service that has a spike in the p99 latencies every x minutes. Sounds familiar? Frequently, it’s a log rotation demon running on the machine where the gzip operation is eating up resources, but more often than not, it is the service itself that causes such spike. It can be a scheduled job that is fetching and processing information, for example periodically refreshing a cache or cleaning up old entries in the DB. If not designed for carefully, such asynchronous execution will impact the p99 latency of the service for synchronous calls.</p>
<p>If you really need to mix such workloads within one application, ensure at least that the http, database connection pools, and thread pools are properly isolated from one another. Otherwise, a long-running async task will impact the synchronous workloads and worst case prevent those from being processed at all.</p>
<h2 id="missing-protection-from-overload-situations">Missing protection from overload situations</h2>
<h3 id="rate-limiting-as-means-to-protect-from-incoming-requestload">Rate limiting as means to protect from incoming request load</h3>
<p>A safe strategy for preventing overload of a service is applying a rate limit to incoming requests. Rate limits are set based on the resources, which impact the scaling ability of the application. These may be driven by its dependencies (e.g. database, 3rd party API) or just costs. Rate limiting can be applied <a href="https://github.com/resilience4j/resilience4j#ratelimiter">within the application itself</a> or outside, for example in API gateways or <a href="https://kubernetes.io/docs/concepts/services-networking/ingress/">ingress</a> controllers. A stricter version of rate limiting is <a href="https://landing.google.com/sre/sre-book/chapters/addressing-cascading-failures/#xref_cascading-failure_load-shed-graceful-degredation">load shedding</a> through request rejection to signal overload situations. To implement load shedding, aside from the technical capabilities to execute this operation, it is helpful to understand per client the business impact of rejecting requests completely as this allows for easy selection of which clients to block first until the service is stabilised.</p>
<p>Rate limits ensure also that clients are forced to negotiate a limit increase with the service provider making scaling needs and capacity planning an explicit conversation. Lastly, rate limiting uncovers and helps dealing with rogue clients of the service. Imagine a service hosting static configuration, which can be cached for a long period of time (e.g. 4 hours). This service should process a request load that is dependent on the number of clients this service has. Load is therefore expected to fluctuate only due to its clients scaling up or down to accommodate incoming load. This service’s load is not expected to follow the traffic patterns of its clients. If it does, it means that such clients are not caching retrieved data correctly and just retrieve it while processing incoming requests.</p>
<h3 id="protection-from-unexpected-request-execution">Protection from unexpected request execution</h3>
<p>While rate limits provide external protection, services must implement internal protection as well. As discussed before, services should define SLOs, on which clients will base their timeout configuration. However, the service itself must be designed to honour this SLO across all operations. This is achieved using timeouts on various levels starting from the persistence layer (e.g. via database statement timeouts) through http connection and request timeouts, up to <a href="http://www.tldp.org/HOWTO/TCP-Keepalive-HOWTO/usingkeepalive.html">TCP keepalives and timeouts</a> and similar operating system settings. Pure compute operations can leverage execution budgets where a maximum execution time is defined, after which the computation will be aborted. If done correctly, it will prevent overload in cases where the calculation time is unexpectedly influenced by the processed data, for example a regex causing <a href="https://swtch.com/~rsc/regexp/regexp1.html">catastrophic backtracking</a>. As <a href="https://blog.cloudflare.com/details-of-the-cloudflare-outage-on-july-2-2019/">past incidents</a> have shown, this is important even for calculation executed in the background, because even though results are not used for the response itself, the calculation consumes CPU cycles and will overload the application anyway.</p>
<h2 id="lack-of-control-of-the-application">Lack of control of the application</h2>
<p>Having the ability to control the applications inner workings is very helpful during incident response. Though it should not be required during normal operations of the service where one relies on reliability patterns, it comes in extremely handy when mitigating incidents. This can be a capability to pause batch jobs, re-trigger processing, or disabling expensive computation in favor of a degraded, but simpler one. Too often, similar changes require a code change and a deployment of the service instead of a simple switch in a feature flag system or via a management API endpoint. The longer the execution of the CI/CD pipeline in the absence of such controls, the worse the ability to quickly react during an incident.</p>
<h2 id="insufficient-visibility">Insufficient visibility</h2>
<p>Aside from monitoring the <a href="https://landing.google.com/sre/sre-book/chapters/monitoring-distributed-systems/">four golden signals</a> (latency, traffic, errors, saturation), further metrics help to gain quick understanding of production incidents. This starts with collecting metrics on connection pools, the rate of incoming requests per client, outgoing requests per dependency, or duration of batch job execution, etc. It can also include metrics very specific to the service itself — for example, if a service offering a batch API is collecting statistics on the batch size, such data can be used to verify whether clients use its API effectively or break it with unexpectedly big batches. Plotting response times per batch size, will provide insights into the processing times and drive discussions on SLOs for the service. Detailed service instrumentation using standard formats, like <a href="https://opentelemetry.io/">OpenTelemetry</a> allows to drill down even further and find causes of incidents across the service call chain as well as identify areas that can be optimised (e.g. through parallelisation of calls) further improving performance and stabilising systems.</p>
<h2 id="summary">Summary</h2>
<p>This post provided an overview of common service design flaws and pitfalls that impact reliability. I can only encourage you to check the out the references provided, especially the <a href="https://landing.google.com/sre/sre-book/toc/">SRE book</a>, which is a great starting point to dive deep into reliability engineering. Further, if you learned something from production issues, please share your <a href="https://k8s.af/">failure stories</a>, so that others can learn from your findings.</p>
]]></content:encoded>
    </item>
    <item>
      <title>Driving change: why are your ideas being rejected?</title>
      <link>https://ocytko.net/posts/driving-change-why-are-your-ideas-being-rejected/</link>
      <pubDate>Wed, 29 Jan 2020 08:16:03 +0000</pubDate>
      <guid>https://ocytko.net/posts/driving-change-why-are-your-ideas-being-rejected/</guid>
      <description>Have you ever felt frustrated when you found a problem worth solving but it was not picked up by your colleagues nor supported by your…</description>
      <content:encoded><![CDATA[<p><em>Originally published on <a href="https://medium.com/@bocytko/driving-change-why-are-your-ideas-being-rejected-e2ab96227e40">medium</a>.</em></p>
<p>Have you ever felt frustrated when you found a problem worth solving but it was not picked up by your colleagues nor supported by your boss? Deep inside you knew you were onto addressing something important, leading to a breakthrough, or just going to have a positive impact on the company, but you failed and don’t know why. This post provides insights on why your ideas may have been rejected along with tips on how to find the right problems to solve.</p>
<h2 id="why-do-managers-reject-yourideas">Why do managers reject your ideas?</h2>
<p>Your personal view on the problems that are important will differ from your manager’s view. Your position, role, and size of the company determine your understanding of the strategic goals of a team, department, or organization and influences the easiness of access to relevant, oftentimes strategic information. Therefore, it’s crucial to understand that not all of the problems that you will identify are worth addressing due to a multitude of reasons:</p>
<ul>
<li><strong>Approach:</strong> The way in which you raised the problem was ineffective. You chose the wrong medium, were too forward and crossed boundaries (e.g. cultural, personal) or just spoke to the wrong person.</li>
<li><strong>Contracts &amp; regulations:</strong> Solving the issue would stand in conflict with the way the company does business due to contractual relationships with customers, the way customers are billed for, or because of regulatory requirements. You may need to build a stronger business case on the positive impacts when addressed, so that the pain points are addressed eventually.</li>
<li><strong>Cost:</strong> Solving the problem would incur too high costs for the company.</li>
<li><strong>Experience</strong>: You’ve joined the team quite recently and/or your manager perceives you as a member of the team who is still learning and trying to understand the mechanics and context of the team. In case you’ve been on the team for a longer time, this may mean that you’re crossing a certain boundary and you should better understand the expectations of your boss, e.g. the areas/problems you should focus on. Also, reflect on the relation with your manager as trust makes a difference.</li>
<li><strong>Focus:</strong> There is another team already working on this challenge and your manager would prefer to keep you focused on the main team’s responsibilities.</li>
<li><strong>Isolation:</strong> You’re the only one that sees this issue and without further support, the idea won’t get further traction.</li>
<li><strong>Priority:</strong> You misunderstood the team’s context and the problem you’re seeing is not worth being solved at this point in time. There may be other opportunities already known that would bring bigger benefits.</li>
<li><strong>Strategy:</strong> Addressing the problem would be in conflict with other, upcoming, more strategic changes you are unaware of. Such changes may be being planned either by your manager or the company.</li>
<li><strong>Value</strong>: It’s unclear what the impact of solving the problem is due to lack of sufficient data points to support your idea.</li>
</ul>
<h2 id="understand-which-problems-are-important">Understand which problems are important</h2>
<p>In order for you to increase chances of a meaningful discussion about the issues you find worth solving, you need a solid understanding of the overall business context and environment. This will help you better showcase the value of resolving the issue to potential supporters. You can start by asking your manager (or other, more senior colleagues) open-ended questions that will help you acquire insights on the big picture and also show the boundaries in which you move in. Here some guiding questions:</p>
<ul>
<li>What is the biggest problem in our team that you see is worth addressing? Why?</li>
<li>I find X annoying/inefficient and would like to help changing that. How could I help finding a solution? Whom in the company would you recommend reaching out to in order to get support?</li>
<li>I think X is an issue in our company. What’s your opinion? Who is responsible within the company to address such problems? With whom beyond this team could I talk to in order to exchange ideas?</li>
<li>When would be a good time to address issue X? Why not earlier?</li>
<li>I have been bringing to you problems that I see as worth solving, but you don’t seem to be interested. Why do you think they’re not worth addressing at this point in time? How would you like me to raise similar issues moving forward?</li>
</ul>
<h2 id="get-support-within-the-organization">Get support within the organization</h2>
<p>Usually, you’re not the only person in the organization that recognized a problem. You just may be the one that is willing to act. However, this does not mean that you’re alone. You can reach out to your colleagues in order to sound your ideas and verify whether others also have this issue and whether they’re willing to support you. This can be as easy as stating that you work on problem X and asking if someone is interested to help. Having support within the organization is crucial as your peers can provide you more context or help addressing the problem in a different way than you would have.</p>
<h2 id="bring-solutions-notproblems">Bring solutions, not problems</h2>
<p>It’s common, especially for less experienced colleagues, to constantly point out problems and deficiencies. Over time, this will lead to fatigue on both sides. You will be frustrated because the problems you raised don’t resonate with your manager and for your boss as well, because you’re likely not the only one to talk about problems. If you never approach your boss with ideas on how to address the problem, you risk being perceived as a complainer and your voice is likely to be ignored over time. Especially from senior team members, managers expect solution suggestions to be brought forward for discussion. Coming with concrete proposals and asking for feedback, context that you may have missed, or for guidance how to proceed in bringing the solution to life will dramatically change the type of discussion you will be having and increase your chances for success. To be on the safe side, you can first validate the idea and come back with a solution proposal only afterwards, which will allow you to focus your attention on the important problems.</p>
<h2 id="its-not-the-problemits-thetiming">It’s not the problem — it’s the timing</h2>
<p>Large organizations are <a href="https://agileandchange.com/7-implications-of-seeing-organisations-as-complex-systems-996fd2398d58">complex systems</a> composed of people. Though a stable state of the organization is not desired, there is a limit to the total amount of change an organization can simultaneously cope with. It may well be, that the problem you found or your idea how to solve a problem is brilliant, but the timing is just not right. This may depend on the company’s current strategic goals, the environment (e.g. market conditions), other ongoing structural or cultural changes, or even the management team itself. In such situations, you can either wait for the circumstances to change or try to vastly reduce the scope of the change and thus aim to make small, incremental steps forward instead.</p>
<h2 id="what-if-all-attemptsfail">What if all attempts fail?</h2>
<p>Surely, the environment or organization you’re in may not be ready to address the issues you’re raising or for the changes you’re willing to drive. If you feel that you’ve been trying for too long and all your attempts failed, you may be better off by changing the environment to another one with a different mindset that is more open for change. This may be a different team, department, or even another organization.</p>
<h2 id="further-reading">Further reading</h2>
<p>This post focused mostly on providing you more understanding on why your ideas would not resonate with others and contains simple guidance on finding the right problems to solve. However, this is just the first step of driving a change. To succeed, you must learn how to define a problem, support it with sufficient context and data, and be able to showcase the impact of solving it.</p>
<p>If you’re more broadly interested in change management, check out <a href="https://www.rebelsatwork.com/resources">Rebels at Work: A Handbook for Leading Change from Within</a> for ideas on building an alliance for change within your organization or <a href="http://www.cs.unca.edu/~manns/intropatterns.html">Fearless Change: Patterns for Introducing New Ideas</a> for an extensive collection of patterns and approaches for driving change.</p>
<p>See also my other posts with advice on <a href="https://ocytko.net/posts/how-to-communicate-major-changes/">Communicating major changes</a> and tips for <a href="https://ocytko.net/posts/accepting-and-supporting-changes/">Accepting and supporting changes</a> for individuals.</p>
]]></content:encoded>
    </item>
    <item>
      <title>Management: your next career step?</title>
      <link>https://ocytko.net/posts/management-your-next-career-step/</link>
      <pubDate>Mon, 06 Nov 2017 19:16:27 +0000</pubDate>
      <guid>https://ocytko.net/posts/management-your-next-career-step/</guid>
      <description>Leadership and management positions are often the next step forward for engineers in their careers. Here some aspects to be considered while transitioning into a management role.</description>
      <content:encoded><![CDATA[<p><em>Originally published on <a href="https://medium.com/@bocytko/management-your-next-career-step-38bd1f9fb742">medium</a>.</em></p>
<p>In many companies leadership and management positions are <em>the</em> next step forward for engineers in their career path and often the only way to achieve a salary progression. Here a glimpse of the various aspects to be considered while thinking of transitioning into a management role.</p>
<h3 id="you-start-as-junioragain">You start as junior again</h3>
<p>After taking up the new role you will be a beginner again. You will need to learn a lot and acquire new skills. If you have watched your past managers closely, you will notice that observing is different from doing. In case you have acquired some leadership skills beforehand you will have a head start.</p>
<h3 id="youre-part-of-the-management-team">You’re part of the management team</h3>
<p>Your peers are not the people you manage, but other managers in the company. Depending on the size of the organisation, there might actually be very few of them (or even spread across different locations). This also means that you will have limited opportunity to learn from other managers on the same level as yours and thus your networking skills will be put to a test.</p>
<p>In case you’re asked to manage your former peers — no matter how well you know them, how well you get along with them — as soon as you become their manager, they will see and treat you as their boss.</p>
<p>Management is hierarchical — you will be asked to execute or communicate things, you not necessarily have worked on, in a way as if you actually had, oftentimes without having all the needed details. Examples include changes in company strategy, restructuring, budget cuts, etc. Some decisions might not be the best for your own teams, but necessary for the long-term success of your company.</p>
<h3 id="youre-responsible-for-the-people-youmanage">You’re responsible for the people you manage</h3>
<p>In order to gain trust, you need to genuinely care for people. It’s your job to create an environment that allows people to take joy in their work, use their abilities and fulfil their potential. Be a role model — your behaviour will influence culture and help to set expectations towards your directs. Remember to be the first to act and take the responsibility to address the matter as soon as it arises.</p>
<p>You will need to manage people’s performance by leveraging feedback, one-on-ones, and conducting periodic progress reviews. While addressing low performance, you cannot forget your top performers that your teams rely upon to succeed. Not managing the growth of your best people (for example by being clingy and blocking their rotation to a new team) along with not living up to their expectation, eventually will make them leave the team or even the company.</p>
<p>It will become personal. You will encounter situations where people on your team will not get along with each other due to personality, attitude, cultural differences, etc. You will be expected to manage and resolve such conflicts within and across teams.</p>
<h3 id="prepare-to-be-judged-by-what-your-teamsdeliver">Prepare to be judged by what your teams deliver</h3>
<p>No matter how great atmosphere you create in the teams, it’s delivery that counts. Delivery against goals or products the teams ship to your customers. Make sure to celebrate successes with the team and praise your directs for the work and efforts they put in.</p>
<h3 id="your-daily-rhythm-willchange">Your daily rhythm will change</h3>
<p>Forget about being able to plan your day. Prepare to be disturbed, over and over again. Being a master meeting scheduler won’t help you. You will find it hard to focus on a single task for a longer time frame and might leave work without the feeling of actually achieving something meaningful. A common error to overcome this feeling is over-involving oneself and not giving enough space to others. Your main job has changed (i.e. it’s not coding anymore), make sure to delegate work and empower others to do it.</p>
<h3 id="is-it-worthit">Is it worth it?</h3>
<p>Management is a new role. It requires high responsibility, discipline, patience, as well as the desire and ability to effectively work with people. You get to build effective teams, create an environment where individuals work towards a common goal, and watch people grow. The decision to transition into a management role requires a certain level of commitment. You don’t get to “try” nor experience management otherwise.</p>
<p>Consequences for failure in management are large, both on business and personal level. Taking risks and learning from situations in the daily work is how you grow and become better in management.</p>
<p>As manager — in case you hire or promote managers, remember your early days in the new role back in the past and make sure to provide enough support and coaching for your directs. As a beginner — ask for it!</p>
]]></content:encoded>
    </item>
    <item>
      <title>Would you trust your money to a smart contract?</title>
      <link>https://ocytko.net/posts/would-you-trust-your-money-to-a-smart-contract/</link>
      <pubDate>Mon, 10 Jul 2017 22:08:37 +0000</pubDate>
      <guid>https://ocytko.net/posts/would-you-trust-your-money-to-a-smart-contract/</guid>
      <description>ICOs are a hot topic nowadays. Many of the recent write-ups discuss the valuation aspect of token crowdsales. In this post I will take a…</description>
      <content:encoded><![CDATA[<p><em>Originally published on <a href="https://medium.com/@bocytko/would-you-trust-your-money-to-a-smart-contract-3581063d52b1">medium</a>.</em></p>
<p>ICOs are a hot topic nowadays. Many of the recent write-ups discuss the valuation aspect of token crowdsales. In this post I will take a different angle and explore the code quality of smart contracts used in ICOs.</p>
<p>There are many tools and practices used to ensure decent software quality. Among others: unit-tests, code reviews, continuous integration, static code analysis, code coverage, mutation testing, formal verification, and following security standards like OWASP or PCI DSS. The more critical the software, the more thorough the quality assurance usually is. For closed-source software, one has to rely on industry practices and professionalism of the companies and individuals. For open-source software different rules apply: it’s easy to assess the quality of publicly shared code.</p>
<p>The ecosystem for developing smart contracts has evolved greatly over time. It’s easy now to do the basics: write unit-tests (in Solidity, Python, JavaScript) and even measure the code coverage. Along with bug bounties and 3rd party audits, there should be little doubt about the quality of smart contracts, right? Let’s see how it looks like in practice for some of the recent ICOs.</p>
<h2 id="project-overview">Project overview</h2>
<h3 id="bat">BAT</h3>
<p>BAT <a href="https://github.com/brave-intl/basic-attention-token-crowdsale">contracts</a> have been <a href="https://blog.zeppelin.solutions/basic-attention-token-bat-audit-88bf196df64b">audited</a> and many errors have been fixed before the ICO. Sadly, we have no unit tests nor usage of any templates, just pure code.</p>
<p><strong>Score:</strong> 0</p>
<h3 id="bancor">Bancor</h3>
<p>The Bancor Protocol <a href="https://github.com/bancorprotocol/contracts">contracts</a> are a different beast. Despite using <a href="http://truffleframework.com/">truffle framework</a> and tests, there exist many deficiencies in the test coverage. Especially, the self-implemented <a href="https://github.com/bancorprotocol/contracts/blob/03c74108b0fd96541a85f46d1b105158b90872a8/solidity/contracts/BancorFormula.sol#L116">math functions</a> (exp, log) are insufficiently tested (see <a href="http://hackingdistributed.com/2017/06/19/bancor-is-flawed/">points 20–22</a>). Plus points go for usage of <a href="https://github.com/bancorprotocol/contracts/commit/9e0eddcea0bb95e647ede4aef45956ca35513912">jupyter notebooks</a> for proving formulas and algorithms. Sadly, due to missing CI, some errors in the notebooks are left unnoticed (like undefined testCornercase2).</p>
<p><strong>Score:</strong> 1.5 (low test coverage, jupyter notebooks)</p>
<h3 id="storj">Storj</h3>
<p>Storj <a href="https://github.com/Storj/storj-contracts">contracts</a> use python integration tests and the <a href="https://github.com/OpenZeppelin/zeppelin-solidity">OpenZeppelin</a> contract templates. Plus for a nice readme with instructions for compiling and testing the contracts.</p>
<p><strong>Score:</strong> 3 (tests, template usage, readme)</p>
<h3 id="aragon">Aragon</h3>
<p>Aragon <a href="https://github.com/aragon/aragon-network-token">contracts</a> use both OpenZeppelin and <a href="https://github.com/Giveth/minime">MiniMeToken</a> as templates. Along with CI we also have a quite active project with many pull requests and code-reviews, which are rather expected in an open-source project, but not really that visible in the other contract repositories.</p>
<p><strong>Score:</strong> 4 (tests, template usage, CI, code reviews)</p>
<h3 id="statusim">Status.im</h3>
<p>Status <a href="https://github.com/status-im/status-network-token">contracts</a>, besides being based on MiniMeToken, introduce an interesting novelty: continuous integration, including a build with test execution orchestrated by truffle framework along with <a href="https://travis-ci.org/status-im/status-network-token/jobs/244806338">execution details</a> and migration tests. On top of that we get public <a href="https://github.com/status-im/status-network-token#reviewers-and-audits">audit results</a>. Great transparency.</p>
<p><strong>Score:</strong> 5 (tests, template usage, CI, detailed test results, audit results)</p>
<h2 id="summary">Summary</h2>
<p>A quick glimpse into the source code reveals how serious founders approach product development and ICOs. There are already very good examples of ICO contracts to learn from. As the ecosystem continuously evolves, here some projects and initiatives to follow (and contribute to):</p>
<ul>
<li><a href="https://github.com/OpenZeppelin/zeppelin-solidity">OpenZeppelin Solidity</a> contract templates for crowdsales, tokens, and math libraries</li>
<li>Solidity <a href="https://github.com/ethereum/solidity/issues/1172">mutation testing</a></li>
<li><a href="https://ethereum.stackexchange.com/questions/11092/what-is-formal-verification-and-why-is-it-important-for-smart-contracts">Formal verification</a> of smart contracts</li>
<li>Status <a href="https://github.com/status-im/status-network-token#reviewers-and-audits">audit results</a> with common errors in smart contracts</li>
<li>Smart Contract <a href="https://github.com/ConsenSys/smart-contract-best-practices">best practices</a> by ConsenSys (featuring <a href="https://github.com/ConsenSys/smart-contract-best-practices#integer-overflow-and-underflow">20 cases for overflow and underflow</a>, that hopefully will get easier to tame after more protection is implemented in <a href="https://github.com/ethereum/solidity/issues/796">Solidity</a> or <a href="https://github.com/ethereum/EIPs/issues/159">EVM</a>)</li>
</ul>
<h2 id="final-words">Final words</h2>
<p>What happens if the founders are in for quick ICO money, deploy sloppy code or are unlucky to hit an yet unknown error? In the worst case your funds will be locked forever, like the <a href="https://www.reddit.com/r/ethereum/comments/6c68mw/new_record_holder_appears_lets_congratulate/">$250,000 lost in smart contracts till date</a>. If you have basic coding skills, you are privileged and can review the contract’s source code, or even try it out on the Ethereum testnet. For all others, the usual advice applies: <em>don’t invest money you cannot lose</em>.</p>
<p>So, coming back to the original question: <strong>would you trust your money to a smart contract?</strong></p>
]]></content:encoded>
    </item>
  </channel>
</rss>
