<?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: Daniel Alker</title>
    <description>The latest articles on DEV Community by Daniel Alker (@timmermann).</description>
    <link>https://dev.to/timmermann</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%2Fuser%2Fprofile_image%2F4062713%2F7e395e62-4e4e-4d63-95fe-8e10149bb9a5.png</url>
      <title>DEV Community: Daniel Alker</title>
      <link>https://dev.to/timmermann</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/timmermann"/>
    <language>en</language>
    <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>
