<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Cobrainer</title>
    <description>The latest articles on DEV Community by Cobrainer (cobrainer).</description>
    <link>https://dev.to/cobrainer</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Forganization%2Fprofile_image%2F14596%2Ffd3b3adb-84d2-47ba-a2b1-70d04a14b791.png</url>
      <title>DEV Community: Cobrainer</title>
      <link>https://dev.to/cobrainer</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/cobrainer"/>
    <language>en</language>
    <item>
      <title>Inside Cobrainer's Engineering Workflow with CodeRabbit</title>
      <dc:creator>Chetan Basuray</dc:creator>
      <pubDate>Tue, 03 Nov 2026 11:01:05 +0000</pubDate>
      <link>https://dev.to/cobrainer/inside-cobrainers-engineering-workflow-with-coderabbit-2cnf</link>
      <guid>https://dev.to/cobrainer/inside-cobrainers-engineering-workflow-with-coderabbit-2cnf</guid>
      <description>&lt;p&gt;&lt;em&gt;Context-aware reviews, shared engineering conventions, and a collaboration that improved the integration.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  TL;DR
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Cobrainer's engineering conventions are made explicit through shared CodeRabbit instructions and repository-specific lint configuration.&lt;/li&gt;
&lt;li&gt;CodeRabbit applied that context at an appropriate stage to recommend a shared query abstraction introduced in the same merge request, making its feedback directly actionable by the developer.&lt;/li&gt;
&lt;li&gt;Our teams found and traced unwanted lint findings to a configuration-precedence defect, which CodeRabbit fixed in under two weeks. In this way, authors assess the recommendations and human reviewers retain the merge decision.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fw7xxfx3xrjm1ow33nlt6.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fw7xxfx3xrjm1ow33nlt6.png" alt="The review flow: the author opens a merge request, the reviewer comments first, the author assesses and can challenge a finding, a colleague reviews, then the change merges" width="800" height="251"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;The reviewer goes first. The merge decision stays with a colleague.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;A code review asks more than whether a piece of code is correct. It asks whether the change belongs in this codebase, whether it follows the project's decisions, and whether the proposed remedy uses what the team has already built. Much of what makes an experienced reviewer useful lies in answering those questions together.&lt;/p&gt;

&lt;p&gt;At Cobrainer, we build a Skills Platform that helps organisations understand their jobs, tasks, and skills. We introduced &lt;a href="https://coderabbit.ai/?utm_source=dev.to&amp;amp;utm_medium=blog&amp;amp;utm_campaign=cobrainer"&gt;CodeRabbit&lt;/a&gt; into our engineering workflow to raise issues before a colleague began reviewing, leaving more of that colleague's attention available for behaviour, design, and maintainability. To make its recommendations useful, we had to give it access to the knowledge behind our own reviews.&lt;/p&gt;

&lt;p&gt;Some of that knowledge was explicit: a linter configuration, for example, records which rules a project has chosen. Other expectations were less formal. Engineers knew which abstraction to reach for because they had helped build it, or because they had discussed a similar change before. Our work with CodeRabbit involved expressing those expectations and observing what happened when the reviewer applied them. Two findings made the problem concrete.&lt;/p&gt;

&lt;h2&gt;
  
  
  Making our conventions available to the reviewer
&lt;/h2&gt;

&lt;p&gt;We put CodeRabbit in front of every merge request. The author considers its findings, makes changes where appropriate, and presents the result for a colleague's review. A human reviewer retains the approval decision. In this arrangement, the practical value of an early review depends on the relevance of the work it asks the author to do.&lt;/p&gt;

&lt;p&gt;We kept our shared CodeRabbit configuration in a central repository that projects in the group inherited. This gave review-profile settings and shared instructions a common source. A change to those instructions could itself receive a review, and we could apply it without maintaining separate copies across repositories.&lt;/p&gt;

&lt;p&gt;Writing the instructions required us to examine what we expected from code review. Which patterns were deliberate? Which apparent violations were acceptable in a particular context? When should a developer use a shared abstraction? One path instruction expressed a recurring expectation:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Do flag changed code that duplicates an existing shared capability instead of reusing it.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Applying this instruction requires knowledge of both the change and the surrounding code. The reviewer must find the shared capability, determine whether it applies, and explain the connection to the author. The instruction makes an engineering preference available for use in a specific review.&lt;/p&gt;

&lt;p&gt;Native tool configuration provides another kind of guidance. A repository's &lt;code&gt;pyproject.toml&lt;/code&gt; records its Ruff rule selection. These settings express decisions the team has already made, and the integrated tool needs to respect them.&lt;/p&gt;

&lt;p&gt;We also compared review profiles using a probe containing nine deliberate defects. The comparison profile reported four; assertive reported seven and left five additional negative cases unflagged. We chose assertive. The small test gave us a reason for that choice, though it could not tell us how accurately either profile would perform across production changes.&lt;/p&gt;

&lt;h2&gt;
  
  
  Applying an abstraction within the change that introduced it
&lt;/h2&gt;

&lt;p&gt;Three weeks into the rollout, a merge request introduced a typed expression API for storage queries. Two predicates in the same change still used string interpolation. One had the following shape, shown here in a simplified example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;urls_for_platform&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;platform&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;AsyncIterator&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;list&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;]]:&lt;/span&gt;
    &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;batch&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stream&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;where&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;platform = &lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;platform&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;'"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;columns&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;url&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="k"&gt;yield&lt;/span&gt; &lt;span class="n"&gt;batch&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The interpolation is easy to recognise. Deciding what to replace it with requires more context. CodeRabbit's comment connected the &lt;code&gt;platform&lt;/code&gt; and &lt;code&gt;source&lt;/code&gt; values to a registry populated through an ingest CLI and HTTP API. The query code therefore could not assume that they were quote-free constants.&lt;/p&gt;

&lt;p&gt;The comment also identified &lt;code&gt;store.expressions.Columns&lt;/code&gt;, introduced in the same merge request, and its literal-escaping path through &lt;code&gt;lit(...).to_sql()&lt;/code&gt;. This gave the author a remedy that already belonged to the project. CodeRabbit recommended using it for both predicates:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;_columns&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Columns&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;CorpusRecord&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;urls_for_platform&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;platform&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;AsyncIterator&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;list&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;]]:&lt;/span&gt;
    &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;batch&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stream&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;where&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nf"&gt;sql&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;_columns&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;platform&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="n"&gt;platform&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="n"&gt;columns&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;url&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="k"&gt;yield&lt;/span&gt; &lt;span class="n"&gt;batch&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F5ydpezyoqmtxza0bwqze.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F5ydpezyoqmtxza0bwqze.png" alt="The finding as it appeared on the merge request, with the code redacted" width="800" height="223"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;The finding as it appeared on the merge request. Identifiers and code are redacted.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;The recommendation applied our reuse instruction to an incomplete adoption of a new abstraction. The expression layer existed, but these call sites had not yet adopted it. Bringing them into the same design reduced the need for each caller to handle literal construction independently.&lt;/p&gt;

&lt;p&gt;Our engineers supplied the abstraction and the convention; CodeRabbit connected them at the point where the author could act. The author assessed the finding and made the change. The review comment records path instructions as its source and marks the finding as addressed in a subsequent commit.&lt;/p&gt;

&lt;h2&gt;
  
  
  Respecting repository policy and the scope of a refactor
&lt;/h2&gt;

&lt;p&gt;A second finding showed how easily the meaning of a review comment can depend on facts outside the line it highlights.&lt;/p&gt;

&lt;p&gt;In &lt;code&gt;app/logging/__init__.py&lt;/code&gt;, a merge request extracted log-level selection into a new helper, &lt;code&gt;_configured_level() -&amp;gt; int&lt;/code&gt;. The existing &lt;code&gt;_setup_handlers()&lt;/code&gt; signature appeared further down as an added line in the diff. CodeRabbit's Ruff-backed finding requested a return annotation on that existing function, citing &lt;code&gt;ANN202&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The engineer pointed out that the signature predated the refactor, while the new helper already carried a return annotation. The repository also selected a specific set of Ruff rules:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight toml"&gt;&lt;code&gt;&lt;span class="k"&gt;[&lt;/span&gt;&lt;span class="n"&gt;tool&lt;/span&gt;&lt;span class="k"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ruff&lt;/span&gt;&lt;span class="k"&gt;.&lt;/span&gt;&lt;span class="n"&gt;lint&lt;/span&gt;&lt;span class="k"&gt;]&lt;/span&gt;
&lt;span class="n"&gt;select&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s"&gt;"E4"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"E7"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"E9"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"F"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"I"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="n"&gt;ignore&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Under Ruff's rule-selection semantics, this selection does not enable the ANN family. The engineer reported that the repository's Ruff check passed on the file.&lt;/p&gt;

&lt;p&gt;CodeRabbit inspected &lt;code&gt;pyproject.toml&lt;/code&gt;, quoted the selection, and withdrew its suggestion. Its reply also acknowledged that the function signature was unchanged.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fvdaomg5tjnsn8rvp0qam.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fvdaomg5tjnsn8rvp0qam.png" alt="The reviewer checks the repository configuration after being challenged, and withdraws its finding" width="800" height="621"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Challenged on the finding, the reviewer reads the repository configuration and withdraws it.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;There were two distinct questions here: had the change introduced the issue, and did the reported rule apply to this project? The diff alone did not settle the first; knowledge of the rule alone did not settle the second. Examining the refactor and the repository configuration changed the conclusion. That exchange gave us a concrete example of how an author could challenge a finding with evidence.&lt;/p&gt;

&lt;h2&gt;
  
  
  Making project policy hold across the integration
&lt;/h2&gt;

&lt;p&gt;Resolving the comment left a further question. Why had a rule outside the repository's selection reached the review at all?&lt;/p&gt;

&lt;p&gt;The findings identified their sources. The SQL recommendation came from path instructions, while the annotation finding came from the linter integration. This distinction helped us investigate the unwanted comments at the level where they originated: the configuration supplied to the integrated tool.&lt;/p&gt;

&lt;p&gt;We collected inline findings from the rollout, grouped them by the analysis tool cited, and extracted their rule codes. Eleven findings named a specific rule:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Findings&lt;/th&gt;
&lt;th&gt;What the configuration check showed&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;8&lt;/td&gt;
&lt;td&gt;The checked-in configuration did not enable the reported rule&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;The project had no such configuration, so the fallback behaviour was expected&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The three expected findings corrected an assumption on our side. We had expected a project policy that the repository did not contain. The other eight provided concrete cases for CodeRabbit's engineering team.&lt;/p&gt;

&lt;p&gt;They reproduced the issue and identified a precedence defect: a fallback configuration could take priority over the repository's checked-in configuration. They explained the cause, shipped a fix, and told us when it was available. The interval from our detailed report to the delivered fix was under two weeks.&lt;/p&gt;

&lt;p&gt;The work depended on both teams. We could compare the comments with the policies and code they were meant to reflect. CodeRabbit's engineers could identify and correct the integration behaviour that produced the mismatch.&lt;/p&gt;

&lt;p&gt;We then observed five days of real merge requests:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Measure&lt;/th&gt;
&lt;th&gt;Result&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Merge requests reviewed&lt;/td&gt;
&lt;td&gt;33&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Merge requests containing Python&lt;/td&gt;
&lt;td&gt;23&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Inline findings across CodeRabbit reviews&lt;/td&gt;
&lt;td&gt;46&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Findings attributed to the affected integration&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Instances of excluded rules counted in reviewed files&lt;/td&gt;
&lt;td&gt;2,392&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The unwanted findings did not recur during this period, while the broader review workflow continued to report findings.&lt;/p&gt;

&lt;p&gt;Interpreting that absence required care. Our first attempt had used two merge requests without Python. Their quiet results could not tell us anything about the affected Python analysis. Even with relevant input, the table supports a limited conclusion: we observed no recurrence. Activity elsewhere in CodeRabbit does not establish successful Ruff execution, and patterns present in reviewed files are not necessarily eligible for inline reporting.&lt;/p&gt;

&lt;h2&gt;
  
  
  Bringing engineering conventions into everyday review
&lt;/h2&gt;

&lt;p&gt;The two examples show different uses of project knowledge. In the query change, the shared abstraction and reuse instruction supported a specific recommendation. In the logging refactor, the rule selection and the history of the function explained why a recommendation did not apply. The collaboration with CodeRabbit's engineers addressed how that rule selection reached the integrated tool.&lt;/p&gt;

&lt;p&gt;For our team, this made explicit conventions a practical part of adopting AI review. Instructions under version control can be revised. Repository policy can be inspected. A finding can be accepted or challenged against those sources. CodeRabbit contributes recommendations and context, while authors and human reviewers remain responsible for the changes that merge.&lt;/p&gt;

&lt;p&gt;A useful starting point for another team is to examine the comments its reviewers repeatedly make. Some may express knowledge that could be written down and supplied to the reviewer. Others may expose decisions the team has never made explicit. Observing how those instructions affect real reviews provides a way to refine both the guidance and the workflow.&lt;/p&gt;

&lt;p&gt;Our work with CodeRabbit gave us examples of that process and a direct collaboration when the integration needed correction. We thank its engineering team for their part in making our project's conventions more consistently available to the review.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>agents</category>
      <category>sdlc</category>
      <category>codereview</category>
    </item>
    <item>
      <title>Your Job Is a Bundle of Tasks. AI Is Unbundling It.</title>
      <dc:creator>Daniel Alker</dc:creator>
      <pubDate>Fri, 30 Oct 2026 07:32:20 +0000</pubDate>
      <link>https://dev.to/cobrainer/your-job-is-a-bundle-of-tasks-ai-is-unbundling-it-32fk</link>
      <guid>https://dev.to/cobrainer/your-job-is-a-bundle-of-tasks-ai-is-unbundling-it-32fk</guid>
      <description>&lt;p&gt;When I joined Cobrainer, my title was Cloud Platform Engineer. In 2024 that title made sense: design the AWS architecture, maintain backend systems, keep the platform running. In 2026 it no longer describes the work. I do not think of myself as a platform engineer any more, but as part of Product &amp;amp; Engineering: someone who contributes to the whole product, not one layer of the stack.&lt;/p&gt;

&lt;p&gt;There was no big announcement. The work changed one task at a time, until the title stopped describing my day.&lt;/p&gt;

&lt;p&gt;I still own the platform. But I also translate product requirements into architecture, ship frontend and backend features, and build services where agents act under human approval. Writing the instructions those agents follow is now part of the job itself.&lt;/p&gt;

&lt;p&gt;That opens up a lot of possibilities. It also raises an uncomfortable question: when an agent takes over work you spent years learning to do, where does your contribution begin and end?&lt;/p&gt;

&lt;p&gt;My answer is taking shape in the work: deciding what matters, giving agents enough context, and knowing when their output is safe to use. &lt;strong&gt;The execution is increasingly shared. The responsibility is still mine.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;That is why I think the useful starting point for AI transformation is the work inside a role: its tasks, its handoffs, and the decisions someone still has to own.&lt;/p&gt;

&lt;h2&gt;
  
  
  What changed in my day
&lt;/h2&gt;

&lt;p&gt;This is one person's experience, not a forecast for every engineer. But putting the tasks side by side makes the shift visible.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Task&lt;/th&gt;
&lt;th&gt;A year ago&lt;/th&gt;
&lt;th&gt;Today&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Shape product and roadmap decisions&lt;/td&gt;
&lt;td&gt;Rarely&lt;/td&gt;
&lt;td&gt;Regularly, with product&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Translate product requirements into architecture and acceptance criteria&lt;/td&gt;
&lt;td&gt;Occasionally&lt;/td&gt;
&lt;td&gt;Core of the job&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Design tenant-isolated cloud architectures&lt;/td&gt;
&lt;td&gt;Me&lt;/td&gt;
&lt;td&gt;Me, with agents exploring options and drafting decision records&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Provision infrastructure as code&lt;/td&gt;
&lt;td&gt;Me and agents&lt;/td&gt;
&lt;td&gt;Agents draft; I verify the plan and blast radius&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Build frontend and backend capabilities&lt;/td&gt;
&lt;td&gt;Me and agents, mostly backend&lt;/td&gt;
&lt;td&gt;Mostly agents; I review based on risk&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Build agent execution services with human approval and audit trails&lt;/td&gt;
&lt;td&gt;Did not exist&lt;/td&gt;
&lt;td&gt;New, and growing&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Validate security and resilience&lt;/td&gt;
&lt;td&gt;Me, partly manual&lt;/td&gt;
&lt;td&gt;Agents write and run tests; I approve the release&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Operate observability and incident recovery&lt;/td&gt;
&lt;td&gt;Me&lt;/td&gt;
&lt;td&gt;Agents investigate; I decide&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Write specs and agent instructions&lt;/td&gt;
&lt;td&gt;Regularly&lt;/td&gt;
&lt;td&gt;Core of the job&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The responsibilities in the old description are still here, but much of my effort has moved towards defining and checking the work. Alongside that, new tasks have appeared: designing approval boundaries, making agent actions traceable, and writing instructions precise enough to act on.&lt;/p&gt;

&lt;p&gt;“Agents write the code” sounds simple. Deciding whether that code solves the right problem, respects tenant boundaries, and can be operated at three in the morning is still engineering.&lt;/p&gt;

&lt;p&gt;Anthropic describes a similar shift in its own workforce. Work on agent instructions, reviews and monitoring grew from 10% of the average employee's interactive Claude sessions in fall 2025 to 22% in spring 2026. Its report also describes human attention becoming a bottleneck as agents produce more output. This is evidence from one unusually AI-intensive company, not a prediction for every workplace. But the growing effort required to direct and review agents is a useful signal. &lt;a href="https://www.anthropic.com/research/employment-at-anthropic" rel="noopener noreferrer"&gt;1&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  A job title hides too much
&lt;/h2&gt;

&lt;p&gt;Two engineers can have the same title and spend their weeks doing very different things. One might maintain a stable service; another might spend most of their time untangling requirements across teams. Their exposure to automation will differ too.&lt;/p&gt;

&lt;p&gt;This distinction predates today's agents. In 2016, Arntz, Gregory and Zierahn estimated that about 9% of jobs across 21 OECD countries were automatable under their task-based approach, substantially below earlier occupation-based estimates. They accounted for differences in what people actually do within the same occupation. That number is not a forecast for today's AI. The lasting lesson is that the unit of analysis matters. &lt;a href="https://doi.org/10.1787/5jlz9h56dvq7-en" rel="noopener noreferrer"&gt;2&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;For engineers, this should feel familiar. You would not estimate a system's performance from its name in the service catalogue. You would look at what it does and where it spends its time.&lt;/p&gt;

&lt;p&gt;We need that same curiosity about our work.&lt;/p&gt;

&lt;h2&gt;
  
  
  Follow the handoffs
&lt;/h2&gt;

&lt;p&gt;Even a task can hide a whole workflow. “Provision infrastructure” might include clarifying requirements, drafting a change, checking permissions, reviewing the plan, applying it, and verifying the result.&lt;/p&gt;

&lt;p&gt;Demirer and colleagues model work as chains of interdependent steps. In their model, the cost of handing work between humans and AI can make it worthwhile to automate a whole sequence, even when a human would perform one isolated step better. Where a step sits in the chain matters. &lt;a href="https://www.nber.org/papers/w34859" rel="noopener noreferrer"&gt;3&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Consider an agent that drafts an infrastructure change and runs its checks. If I have to reconstruct its reasoning before I can approve the plan, generating the code faster may save less time than expected. If it gives me a clear diff, relevant test results and a credible rollback path, the review becomes easier.&lt;/p&gt;

&lt;p&gt;The approval still serves a purpose. The engineering challenge is to give the person approving the change enough evidence to make a good decision.&lt;/p&gt;

&lt;p&gt;“Six out of ten steps are automatable” tells me very little until I know how those steps connect.&lt;/p&gt;

&lt;h2&gt;
  
  
  Ask what people want to keep
&lt;/h2&gt;

&lt;p&gt;There is a human question here that a capability benchmark cannot answer.&lt;/p&gt;

&lt;p&gt;Shao and colleagues gathered preferences from 1,500 workers across 104 occupations and compared them with AI experts' assessments of technical capability. The correlation between workers' desire for automation and assessed capability was weak: Spearman ρ = 0.17. Feasible automation and wanted automation often pointed to different tasks. &lt;a href="https://arxiv.org/abs/2506.06576v3" rel="noopener noreferrer"&gt;4&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;That matters because work is also a source of pride, identity and learning. A task someone wants to hand off may be the task a colleague enjoys most. And work an experienced engineer can safely delegate may be how a junior engineer learns to recognise a bad solution.&lt;/p&gt;

&lt;p&gt;I think we owe people a better conversation than “this percentage of your job can be automated.” Start with what frustrates them, what they value, and what they need to learn. Then work through what should change together.&lt;/p&gt;

&lt;h2&gt;
  
  
  Start with one role and a real week of work
&lt;/h2&gt;

&lt;p&gt;For engineering and HR leaders, I would start small:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Map the work with the people doing it.&lt;/strong&gt; Take one role, list its tasks, and break a few into steps. Include the interruptions and judgement calls that never made it into the job description.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Trace one workflow end to end.&lt;/strong&gt; Mark where AI can assist, where it can complete a step, and where a person must decide. Look closely at the handoffs and the evidence needed for review.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Separate capability from preference.&lt;/strong&gt; Ask both what AI can do reliably today and what people want help with. Record uncertainty and revisit the assessment as tools and workflows change.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Make room for the new work.&lt;/strong&gt; Instructions, review, monitoring and recovery take time. Put them into role descriptions and development plans, including how less experienced colleagues will learn.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Agree how the map will be used.&lt;/strong&gt; Use it to redesign work with the team. It should not become a ranking of whose job is easiest to remove. Involve employee representatives early where relevant.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The first map will be incomplete. It only needs to be concrete enough for someone to say, “That is not how this task actually works,” and help you improve it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why we are working on this at Cobrainer
&lt;/h2&gt;

&lt;p&gt;At Cobrainer, we are building on this idea across our Work Architecture, Talent Intelligence and AI Transformation capabilities: making the tasks inside roles visible, so organisations and their people can understand and shape how the work changes.&lt;/p&gt;

&lt;p&gt;We will share more in our Fall Release, followed by posts about the engineering behind it.&lt;/p&gt;

&lt;p&gt;For me, this is already personal. The title I joined with describes a 2024 job. In 2026 the line between product and engineering has blurred: I contribute across the whole product, while the decisions I make still carry consequences for the platform and the people using it.&lt;/p&gt;

&lt;p&gt;A useful task map gives us somewhere to start that conversation. It lets us talk about what is changing while there is still time to shape it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Which part of your work has changed most this year, and how do you feel about that change?&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  References
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;Wilmers, N., Catipon, R., Massenkoff, M., &amp;amp; McCrory, P. (2026). &lt;a href="https://www.anthropic.com/research/employment-at-anthropic" rel="noopener noreferrer"&gt;&lt;em&gt;How employment is changing at Anthropic&lt;/em&gt;&lt;/a&gt;. Anthropic.&lt;/li&gt;
&lt;li&gt;Arntz, M., Gregory, T., &amp;amp; Zierahn, U. (2016). &lt;a href="https://doi.org/10.1787/5jlz9h56dvq7-en" rel="noopener noreferrer"&gt;&lt;em&gt;The Risk of Automation for Jobs in OECD Countries: A Comparative Analysis&lt;/em&gt;&lt;/a&gt;. OECD Social, Employment and Migration Working Papers No. 189.&lt;/li&gt;
&lt;li&gt;Demirer, M., Horton, J. J., Immorlica, N., Lucier, B., &amp;amp; Shahidi, P. (2026). &lt;a href="https://www.nber.org/papers/w34859" rel="noopener noreferrer"&gt;&lt;em&gt;Chaining Tasks, Redefining Work: A Theory of AI Automation&lt;/em&gt;&lt;/a&gt;. NBER Working Paper 34859.&lt;/li&gt;
&lt;li&gt;Shao, Y., Zope, H., Jiang, Y., Pei, J., Nguyen, D., Brynjolfsson, E., &amp;amp; Yang, D. (2026). &lt;a href="https://arxiv.org/abs/2506.06576v3" rel="noopener noreferrer"&gt;&lt;em&gt;Future of Work with AI Agents: Auditing Automation and Augmentation Potential across the U.S. Workforce&lt;/em&gt;&lt;/a&gt;. arXiv:2506.06576v3.&lt;/li&gt;
&lt;/ol&gt;

</description>
      <category>ai</category>
      <category>career</category>
      <category>leadership</category>
      <category>futureofwork</category>
    </item>
    <item>
      <title>The JVM Speaks AI: Cobrainer's Journey with Kotlin, Koog and Exposed</title>
      <dc:creator>Daniel Alker</dc:creator>
      <pubDate>Tue, 13 Oct 2026 07:51:48 +0000</pubDate>
      <link>https://dev.to/cobrainer/the-jvm-speaks-ai-cobrainers-journey-with-kotlin-koog-and-exposed-386h</link>
      <guid>https://dev.to/cobrainer/the-jvm-speaks-ai-cobrainers-journey-with-kotlin-koog-and-exposed-386h</guid>
      <description>&lt;p&gt;One afternoon, one of our LLM providers started throttling us. A large customer had triggered a burst of AI-assisted work, and the provider's capacity in our region could not keep up. Our prompt executor followed its fallback chain, the circuit breaker took the affected models out of rotation, and each request moved to a second provider without interrupting the workflow. There was no incident to handle and no change in system behaviour for the customer. We noticed it only the next morning on our LLM observability dashboard.&lt;/p&gt;

&lt;p&gt;We built all of our AI integration in Kotlin. A controversial decision? We do not think so. In 2025, one could still argue that AI integration required Python. By 2026, that debate is no longer a useful basis for architecture decisions. Python remains important for research and model training, but production AI is mostly about integrating APIs, enforcing contracts, and operating reliable systems. Every prompt, agent, and structured-output contract in our core platform lives on the JVM: one Kotlin monorepo with types shared end to end, built on &lt;a href="https://github.com/JetBrains/koog" rel="noopener noreferrer"&gt;Koog&lt;/a&gt;, &lt;a href="https://github.com/JetBrains/Exposed" rel="noopener noreferrer"&gt;Exposed&lt;/a&gt;, coroutines, and kotlinx.serialization.&lt;/p&gt;

&lt;h2&gt;
  
  
  TL;DR
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Kotlin lets six developers and their coding agents work in one stack. Sealed types, kotlinx.serialization, null safety, and coroutines make contracts and control flow explicit.&lt;/li&gt;
&lt;li&gt;Koog was the best fit because its lightweight abstractions build on Kotlin instead of hiding it. We moved the proof of concept into production for all customers in one day.&lt;/li&gt;
&lt;li&gt;Production reliability comes from cross-provider fallback chains, circuit breakers, LLMObs, and contract tests. Removing blocking code from coroutine paths also increased throughput by &lt;strong&gt;5×&lt;/strong&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  We at Cobrainer
&lt;/h2&gt;

&lt;p&gt;Cobrainer is a Munich-based B2B HR-tech company that grew out of a Technical University of Munich research project in 2013. We serve enterprises across industries and regions. Our AI-native Skills Platform creates and governs living Job and Skill Architectures: structured models of an organization's functional roles, levels, tasks, and skills. They answer three basic questions: Which distinct jobs exist in the organization? What is needed to perform them? How do they relate to each other? A patented Skills Graph and ML/NLP power the platform.&lt;/p&gt;

&lt;p&gt;Our customers use it for skills-based (AI) workforce transformation: internal mobility, recruiting, learning, workforce planning, and EU pay-transparency readiness, with integrations into SAP SuccessFactors, Workday, ServiceNow and other HCM systems.&lt;/p&gt;

&lt;p&gt;Our Summer 2026 release includes a context-aware Personal Agent for the Job Architecture and the wider platform, agentic background tasks, agentic (g)RAG, and collaborative multilingual commenting for governance. The AI can resolve structural and language-specific comments on job profiles in more than 25 languages.&lt;/p&gt;

&lt;p&gt;Our agentic engineering setup centres on six developers with end-to-end ownership, from cloud infrastructure through backend and frontend to AI. Coding agents help us explore the repository, implement changes, run checks, and investigate operational issues. Together, we serve more than 10,000 active users at companies around the world.&lt;/p&gt;

&lt;p&gt;Small, fast-moving team, big surface. That constraint explains almost every technology decision in this article.&lt;/p&gt;

&lt;h2&gt;
  
  
  Kotlin for AI-powered features
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Why Kotlin? Reliability meets velocity
&lt;/h3&gt;

&lt;p&gt;The control plane and core domain of our platform use Kotlin and Spring Boot. The stack is type-safe and null-safe end to end, with kotlinx.serialization for wire contracts, Exposed for SQL, and coroutines for non-blocking I/O. A central monolith is the entry point for the platform, and everything lives in one monorepo. With six engineers working across the full system, every additional deployable adds work to development, operations, and CI.&lt;/p&gt;

&lt;p&gt;A large monolith rewards explicitness. Its behaviour can span distant files, and coding agents cannot safely fill gaps left by implicit conventions. Kotlin's types and compiler checks make those relationships visible. Koog follows the same principle with small abstractions that keep contracts, capabilities, and control flow in the code. This helps developers review changes and gives coding agents firmer boundaries in which to work.&lt;/p&gt;

&lt;p&gt;When our LLM work moved from isolated features to core platform components, we asked whether it justified a separate stack. The models sit behind HTTP APIs, so another runtime would not improve their output. It would give us a second deployment pipeline, add serialization boundaries between the AI layer and domain model, and remove compile-time guarantees at the boundary where LLMs introduce the most uncertainty.&lt;/p&gt;

&lt;p&gt;We wrote down what we actually needed from an LLM framework:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Lean and explicit.&lt;/strong&gt; The framework should behave like a library while keeping control flow visible in the code.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A release cadence that matches innovation speed and model churn.&lt;/strong&gt; New models and paradigms arrive monthly, so infrequent framework releases become a bottleneck.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Clean provider and model-capability abstractions.&lt;/strong&gt; We need multiple providers, and their models support different features.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Type safety end to end.&lt;/strong&gt; An LLM's output should enter the domain as a typed value or not at all.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;We tested these requirements by building working implementations instead of comparing feature matrices.&lt;/p&gt;

&lt;h3&gt;
  
  
  The turning point: why Koog fits Kotlin
&lt;/h3&gt;

&lt;p&gt;We built and ran production implementations with Spring AI, LangChain4j, and Koog. The important difference was not the length of their feature lists. It was how clearly each framework expressed its behaviour in Kotlin for developers, coding agents, and production tooling.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Spring AI&lt;/strong&gt; is the most comprehensive and deeply integrated Spring option. It offers a robust ecosystem and good portability for teams already using Spring Boot. We still use it for our MCP endpoints, where its integration with REST infrastructure and the Spring Security filter chain is exactly what we need. In the agentic core, however, it added another layer between our code and the model when we wanted direct control and rapid iteration.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;LangChain4j&lt;/strong&gt; is a large, feature-rich library that brings LangChain concepts to the JVM. It helps teams move quickly, but we found failures inside a chain difficult to trace back to the underlying LLM call. We eventually replaced the last utilities that depended on it with a few dozen lines of Kotlin.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Koog&lt;/strong&gt; changed the direction of the evaluation because it made Kotlin itself the foundation of the integration. Our &lt;code&gt;@Serializable&lt;/code&gt; domain types become structured-output contracts directly. Sealed hierarchies model the possible results, and exhaustive &lt;code&gt;when&lt;/code&gt; expressions force every caller to handle them. Coroutines carry naturally through provider calls, retries, and agent workflows. Koog's DSL builds on familiar Kotlin features such as extension functions and lambdas with receivers, so agent graphs remain normal Kotlin code that can be traced, changed, and verified with the compiler.&lt;/p&gt;

&lt;p&gt;The provider abstraction was equally important. Koog represents model capabilities explicitly instead of reducing every model to a lowest common denominator. We can adapt temperature, token limits, and structured-output handling for each model while keeping the same domain contract and call site. These decisions are visible in types and code instead of being hidden in configuration or prompts. When a developer or agent investigates a failure, the underlying request remains accessible rather than disappearing behind a chain of abstractions.&lt;/p&gt;

&lt;p&gt;That combination gave us the level of abstraction we wanted: enough structure to avoid rebuilding provider clients and agent orchestration, but close enough to Kotlin that our existing tools, types, and debugging habits still applied. A coding agent can trace a change from a domain type through its LLM contract to every consumer without crossing languages or repositories. Koog did not introduce a second programming model into the codebase. It made the existing one more useful to both humans and agents.&lt;/p&gt;

&lt;p&gt;Once we chose Koog, we cleaned up the proof of concept and rolled it out to all customers &lt;strong&gt;in one day&lt;/strong&gt;. That speed came from keeping the domain model, serialization, concurrency model, tests, and deployment pipeline we already understood.&lt;/p&gt;

&lt;h2&gt;
  
  
  AI in production: structured output as the workhorse
&lt;/h2&gt;

&lt;p&gt;In production, much of an LLM's value comes from filling data structures that domain logic can trust, rather than generating free-form chat. Koog has powered our Job Architecture generation since summer 2025, and structured output is the foundation of that core product capability.&lt;/p&gt;

&lt;p&gt;The contract is a Kotlin sealed hierarchy with kotlinx.serialization. Koog turns it into a JSON schema (docstrings included, via &lt;code&gt;@LLMDescription&lt;/code&gt;), gets the provider to comply, and hands back a typed value:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight kotlin"&gt;&lt;code&gt;&lt;span class="nd"&gt;@Serializable&lt;/span&gt;
&lt;span class="k"&gt;enum&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;ChangeKind&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nc"&gt;RENAME&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;ARCHIVE&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;MOVE&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nd"&gt;@LLMDescription&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"A single proposed change to the job architecture"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nd"&gt;@Serializable&lt;/span&gt;
&lt;span class="k"&gt;sealed&lt;/span&gt; &lt;span class="kd"&gt;interface&lt;/span&gt; &lt;span class="nc"&gt;ProposedChange&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;kind&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;ChangeKind&lt;/span&gt;

    &lt;span class="nd"&gt;@Serializable&lt;/span&gt;
    &lt;span class="nd"&gt;@SerialName&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"rename"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="kd"&gt;data class&lt;/span&gt; &lt;span class="nc"&gt;Rename&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="err"&gt;@&lt;/span&gt;&lt;span class="n"&gt;property&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="nc"&gt;LLMDescription&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"The item to rename"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;itemId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="err"&gt;@&lt;/span&gt;&lt;span class="n"&gt;property&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="nc"&gt;LLMDescription&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"New title, in the tenant's base language"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;newTitle&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;ProposedChange&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;override&lt;/span&gt; &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;kind&lt;/span&gt; &lt;span class="k"&gt;get&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;ChangeKind&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;RENAME&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="nd"&gt;@Serializable&lt;/span&gt;
    &lt;span class="nd"&gt;@SerialName&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"archive"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="kd"&gt;data class&lt;/span&gt; &lt;span class="nc"&gt;Archive&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="err"&gt;@&lt;/span&gt;&lt;span class="n"&gt;property&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="nc"&gt;LLMDescription&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Items to archive, including their descendants"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;itemIds&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;List&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;,&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;ProposedChange&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;override&lt;/span&gt; &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;kind&lt;/span&gt; &lt;span class="k"&gt;get&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;ChangeKind&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;ARCHIVE&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="c1"&gt;// ...&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Requesting it is one call. If a weaker model returns almost-JSON, a &lt;code&gt;StructureFixingParser&lt;/code&gt; repairs it with a lightweight model before we ever see it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight kotlin"&gt;&lt;code&gt;&lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;plan&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;executor&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;executeStructured&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;ChangePlan&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;(&lt;/span&gt;
    &lt;span class="n"&gt;prompt&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;prompt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"change-planner"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nf"&gt;system&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;plannerInstructions&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="nf"&gt;user&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="n"&gt;model&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;fixingParser&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;StructureFixingParser&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;model&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;lightweightModel&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;retries&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;getOrThrow&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;

&lt;span class="n"&gt;plan&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;changes&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;forEach&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;change&lt;/span&gt; &lt;span class="p"&gt;-&amp;gt;&lt;/span&gt;
    &lt;span class="k"&gt;when&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;change&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="c1"&gt;// The compiler enforces exhaustive coverage.&lt;/span&gt;
        &lt;span class="k"&gt;is&lt;/span&gt; &lt;span class="nc"&gt;ProposedChange&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Rename&lt;/span&gt; &lt;span class="p"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;preview&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;change&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;itemId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;change&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;newTitle&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;is&lt;/span&gt; &lt;span class="nc"&gt;ProposedChange&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Archive&lt;/span&gt; &lt;span class="p"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;confirmArchive&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;change&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;itemIds&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;is&lt;/span&gt; &lt;span class="nc"&gt;ProposedChange&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Move&lt;/span&gt; &lt;span class="p"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;validateMove&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;change&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Kotlin provides specific safeguards here. Null safety exposes schema drift at compile time: add a required field to the contract and every construction site that omits it stops compiling. A &lt;code&gt;when&lt;/code&gt; expression over a sealed result is exhaustive, so a new change kind breaks the build until every consumer handles it. Default arguments and small DSLs keep configuration readable, allowing us to review prompt code like domain code.&lt;/p&gt;

&lt;p&gt;We also balance traffic across providers whose models differ in capabilities, and Koog represents those differences explicitly. A provider that uses an existing API format requires little integration code:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight kotlin"&gt;&lt;code&gt;&lt;span class="c1"&gt;// xAI uses the OpenAI wire format.&lt;/span&gt;
&lt;span class="nd"&gt;@Serializable&lt;/span&gt;
&lt;span class="kd"&gt;object&lt;/span&gt; &lt;span class="nc"&gt;XAI&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;LLMProvider&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"xai"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"xAI"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;xaiClient&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;OpenAILLMClient&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;apiKey&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;settings&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;apiKey&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;settings&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;OpenAIClientSettings&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;baseUrl&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"https://api.x.ai"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Our executor adds three behaviours to Koog's &lt;code&gt;PromptExecutor&lt;/code&gt;: retry with backoff, a config-driven fallback chain, and a per-model circuit breaker. Before each attempt, it adapts the prompt to the target model's declared capabilities:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight kotlin"&gt;&lt;code&gt;&lt;span class="k"&gt;fun&lt;/span&gt; &lt;span class="nf"&gt;adaptPromptFor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;LLModel&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;prompt&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;Prompt&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nc"&gt;Prompt&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;prompt&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;letIf&lt;/span&gt;&lt;span class="p"&gt;(!&lt;/span&gt;&lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;supports&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;LLMCapability&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Temperature&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;it&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;withoutTemperature&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;letIf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;prompt&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;maxTokens&lt;/span&gt; &lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;maxOutputTokens&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;it&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;cappedTo&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;maxOutputTokens&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="c1"&gt;// Model can't do native structured output? Ask for it in the prompt&lt;/span&gt;
    &lt;span class="c1"&gt;// instead. The sealed contract and fixing parser stay identical.&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;letIf&lt;/span&gt;&lt;span class="p"&gt;(!&lt;/span&gt;&lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;supports&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;prompt&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;schemaCapability&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;it&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;withSchemaAsUserMessage&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The call site never knows which provider answered. That indirection is what made the silent rebalancing from the opening of this article a non-event.&lt;/p&gt;

&lt;h2&gt;
  
  
  Growing into an agentic platform
&lt;/h2&gt;

&lt;p&gt;At Cobrainer, AI powers skill inference, Job Architecture generation, transcription, chat agents, and agentic (g)RAG. Our Job Architecture chat agent brings several of these capabilities together.&lt;/p&gt;

&lt;p&gt;The agent lets users create, review, and maintain a job architecture conversationally: "merge these two levels", "draft the missing task descriptions for this job family", or "archive everything nobody has touched since the reorg". At a high level, it is a &lt;strong&gt;supervisor tool loop with deterministic sub-workflows&lt;/strong&gt;. The LLM decides &lt;em&gt;what&lt;/em&gt; to do, while a Koog strategy graph routes every consequential action through code we control. A &lt;strong&gt;human must approve every change&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;The agent is our one exception to the monolith. It runs in the Agent Runtime, a reactive Spring Boot Kotlin service designed for long-running conversations, streaming, and structured concurrency. The monolith remains the single entry point for the platform, including REST and MCP. Both services live in the same monorepo, so they share types directly and the compiler checks their contract. SurrealDB stores checkpoints for resumable turns, message history, and three layers of memory: a small window of recent conversation chunks, a running session briefing, and a fact-based long-term graph. Platform data remains in Postgres behind the monolith, accessed through Exposed.&lt;/p&gt;

&lt;p&gt;Koog's graph DSL makes the tool loop almost declarative. We use &lt;em&gt;marker tools&lt;/em&gt; that the model can call but that never execute directly. A guarded edge intercepts the call and routes it into a deterministic sub-workflow:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight kotlin"&gt;&lt;code&gt;&lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;toolLoop&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;strategy&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;(&lt;/span&gt;&lt;span class="s"&gt;"supervisor"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;callLlm&lt;/span&gt; &lt;span class="k"&gt;by&lt;/span&gt; &lt;span class="nf"&gt;nodeLLMRequest&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;executeTool&lt;/span&gt; &lt;span class="k"&gt;by&lt;/span&gt; &lt;span class="nf"&gt;nodeExecuteTool&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;sendResult&lt;/span&gt; &lt;span class="k"&gt;by&lt;/span&gt; &lt;span class="nf"&gt;nodeLLMSendToolResult&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

    &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;editFlow&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;installEditCapability&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;approvalGate&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;bulkFlow&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;installBulkCapability&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;approvalGate&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="c1"&gt;// Marker tools are visible to the model but never executed.&lt;/span&gt;
    &lt;span class="c1"&gt;// The edge routes into a deterministic sub-workflow instead.&lt;/span&gt;
    &lt;span class="nf"&gt;edge&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="n"&gt;callLlm&lt;/span&gt; &lt;span class="n"&gt;forwardTo&lt;/span&gt; &lt;span class="n"&gt;editFlow&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;soleMarker&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;EDIT_TOOL_ID&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="nf"&gt;edge&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="n"&gt;callLlm&lt;/span&gt; &lt;span class="n"&gt;forwardTo&lt;/span&gt; &lt;span class="n"&gt;bulkFlow&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;soleMarker&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;BULK_TOOL_ID&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;

    &lt;span class="c1"&gt;// Edges match in declaration order, so the generic loop comes last.&lt;/span&gt;
    &lt;span class="nf"&gt;edge&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;callLlm&lt;/span&gt; &lt;span class="n"&gt;forwardTo&lt;/span&gt; &lt;span class="n"&gt;executeTool&lt;/span&gt; &lt;span class="nf"&gt;onToolCalls&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;true&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;
    &lt;span class="nf"&gt;edge&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;callLlm&lt;/span&gt; &lt;span class="n"&gt;forwardTo&lt;/span&gt; &lt;span class="n"&gt;nodeFinish&lt;/span&gt; &lt;span class="nf"&gt;onAssistantMessage&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;true&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;
    &lt;span class="nf"&gt;edge&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;executeTool&lt;/span&gt; &lt;span class="n"&gt;forwardTo&lt;/span&gt; &lt;span class="n"&gt;sendResult&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The same graph drives the progress UI. Instead of streaming tokens, we show meaningful progress. Koog's event hooks fire as the agent moves through nodes and tools, and each transition maps to a localized status such as "Analyzing your architecture…" or "Preparing changes for review…". Showing graph progress improved perceived latency more than token streaming would have for this workflow:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight kotlin"&gt;&lt;code&gt;&lt;span class="nf"&gt;install&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;EventHandler&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;onNodeExecutionStarting&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;ctx&lt;/span&gt; &lt;span class="p"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;emitStatus&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;node&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="nf"&gt;onToolCallStarting&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;ctx&lt;/span&gt; &lt;span class="p"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;emitToolStatus&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;toolName&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="nf"&gt;onAgentExecutionFailed&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;ctx&lt;/span&gt; &lt;span class="p"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;events&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;close&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Security is where the two frameworks meet. The agent accesses tools only through central MCP servers, which Spring AI serves inside the monolith. Every tool call therefore passes through &lt;strong&gt;the same Spring Security filter chain as our REST APIs&lt;/strong&gt;. We run one MCP server per permission scope (read, architect, reviewer), guard each at the path level, and give the agent a short-lived delegated token so it always acts &lt;em&gt;as the user&lt;/em&gt;. Permissions filter the available tools before any LLM call. If a user cannot edit the architecture, their agent never sees editing tools. This reuses our existing authorization infrastructure without relying on prompts for access control.&lt;/p&gt;

&lt;p&gt;Koog runs the agent in the Agent Runtime, Spring AI exposes and protects its tools in the monolith, Exposed and Postgres store platform data, and SurrealDB stores agent memory. The result is two JVM services in one monorepo, sharing one type system.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F11i1acwhd0wnk7jh7ben.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F11i1acwhd0wnk7jh7ben.png" alt="High-level architecture of the Job Architecture agent" width="799" height="438"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Discoveries and challenges
&lt;/h2&gt;

&lt;p&gt;Production use also exposed several important problems and lessons.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A 5× throughput gain from coroutine cleanup.&lt;/strong&gt; Our most difficult performance problem had nothing to do with Koog or LLMs. Legacy blocking code inside coroutines starved dispatchers under AI-driven load, causing vague and load-dependent slowdowns. Once we found and removed it, throughput on the affected paths increased by &lt;strong&gt;5×&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Which leads to the sharpest lesson we can offer:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Do not leave &lt;code&gt;runBlocking&lt;/code&gt; in production request paths.&lt;/strong&gt; This applies even to a blocking Spring Boot servlet stack where it appears to work. Under load, it can deadlock a dispatcher without crashing the application; requests simply stop completing.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;strong&gt;Build deep fallback chains across providers.&lt;/strong&gt; Several fallback models from the same provider still share one failure domain. Our chains cross provider boundaries, and per-model circuit breakers skip throttled or unhealthy routes so retries reach a viable model quickly. We trace every attempt and fallback in LLMObs, including latency, token usage, and errors. That makes it possible to reconstruct a request across providers and distinguish a model failure from a parser or application problem. This setup paid off as soon as one provider lost capacity.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Contract-test the LLM layer.&lt;/strong&gt; Structured output depends on our schema, the framework's schema generation, and each provider's implementation. We run end-to-end structured-output tests with real calls and parsing for every provider, as well as LLM contract tests in CI. Together with LLMObs, these checks form our AI operations loop: observe production behaviour, investigate failures with full context, and validate model or provider changes before rollout. This lets developers and agents move quickly without turning model churn into guesswork.&lt;/p&gt;

&lt;h2&gt;
  
  
  Final words
&lt;/h2&gt;

&lt;p&gt;Building AI features in Kotlin once sounded unconventional. Today, its explicit contracts and lightweight abstractions give our six developers and their coding agents a system that is fast to change, safe to refactor, and straightforward to operate.&lt;/p&gt;

&lt;p&gt;If you are considering AI development on the JVM, the ecosystem is ready. Have a look at &lt;a href="https://github.com/JetBrains/koog" rel="noopener noreferrer"&gt;Koog&lt;/a&gt;, &lt;a href="https://github.com/JetBrains/Exposed" rel="noopener noreferrer"&gt;Exposed&lt;/a&gt;, and &lt;a href="https://kotlinlang.org" rel="noopener noreferrer"&gt;Kotlin&lt;/a&gt;. We thank the JetBrains teams behind all three for their frequent releases and responsiveness.&lt;/p&gt;

&lt;p&gt;Questions, disagreements, and production stories are welcome in the comments. We read them.&lt;/p&gt;

</description>
      <category>kotlin</category>
      <category>ai</category>
      <category>koog</category>
      <category>jvm</category>
    </item>
  </channel>
</rss>
