<?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: Manuel Alejandro Hermoso</title>
    <description>The latest articles on DEV Community by Manuel Alejandro Hermoso (@cuento3yllevo2).</description>
    <link>https://dev.to/cuento3yllevo2</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%2F4178765%2F02d1f9ca-1068-4eb0-94bf-9d00c9c60795.jpeg</url>
      <title>DEV Community: Manuel Alejandro Hermoso</title>
      <link>https://dev.to/cuento3yllevo2</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/cuento3yllevo2"/>
    <language>en</language>
    <item>
      <title>Scary Truc: a stranger calls you from inside a maze, and your voice is all they have</title>
      <dc:creator>Manuel Alejandro Hermoso</dc:creator>
      <pubDate>Sat, 31 Oct 2026 17:26:31 +0000</pubDate>
      <link>https://dev.to/cuento3yllevo2/scary-truc-a-stranger-calls-you-from-inside-a-maze-and-your-voice-is-all-they-have-15ee</link>
      <guid>https://dev.to/cuento3yllevo2/scary-truc-a-stranger-calls-you-from-inside-a-maze-and-your-voice-is-all-they-have-15ee</guid>
      <description>&lt;p&gt;&lt;em&gt;This is a submission for the &lt;a href="https://dev.to/challenges/hacktoberfest-week4-2026-10-26"&gt;Hacktoberfest Open-Source AI Challenge Week 4: Ghost in the Machine&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What I Built
&lt;/h2&gt;

&lt;p&gt;It's past midnight and your phone is ringing. Unknown number.&lt;/p&gt;

&lt;p&gt;You answer, and someone is whispering on the line. They woke up somewhere they shouldn't be. They don't know how they got there. The people who came with them are gone, and you're the only one who picked up.&lt;/p&gt;

&lt;p&gt;You can't see where they are. You have their voice, a battery that is running out, and a notebook where you draw the maze from what they tell you. The pencil fades. Something else is down there with them. It has no eyes, and it finds them by how they sound. So you talk them through it, left, right, hide, don't breathe, and you keep them calm, because fear is loud.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Scary Truc&lt;/strong&gt; is that phone call. Gemma plays the person on the line, and it also plays the thing hunting them, which sometimes gets on the phone and speaks to you:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;They still bleed for the mistake you called a plan.&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Nobody wrote that line. Gemma did, during a call.&lt;/p&gt;

&lt;p&gt;There are no buttons to choose from. You talk to them the way you'd talk to a terrified stranger, and Gemma reads how you said it as well as where you said to go. A kind word steadies them. Shouting makes them stumble, refuse, or run down a corridor you never chose. Ask them for a joke and they'll wonder who you really are. Go quiet and they'll beg you to say something, and then they'll choose for themselves, badly.&lt;/p&gt;

&lt;p&gt;And they don't know you. Yours is the only number in their phone, and they never saved it. Every so often they stop and ask who you are, or whether you're the one who put them there, and they won't take another step until you answer.&lt;/p&gt;

&lt;p&gt;There are six calls, each a hand-made maze after a horror film: The Cave after &lt;em&gt;The Descent&lt;/em&gt;, The Woods after &lt;em&gt;The Blair Witch Project&lt;/em&gt;, The Cube after &lt;em&gt;Cube&lt;/em&gt;, The Tape after &lt;em&gt;Ring&lt;/em&gt;, The Hedge after &lt;em&gt;The Shining&lt;/em&gt;, and The Ship after &lt;em&gt;Alien&lt;/em&gt;. At midnight a bell tolls, the walls move, and what's in your notebook becomes a memory. When the line goes dead, the phone tells you how many steps they were from the way out.&lt;/p&gt;

&lt;p&gt;One of my calls in The Cave, in Spanish, ended like this:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;I went through the door. I can see the sky. I'm out. Thank you. Who are you?&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Demo
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href="https://deeptelar.github.io/scary-truc/" rel="noopener noreferrer"&gt;Answer the phone&lt;/a&gt;&lt;/strong&gt;. It runs in the browser, and it's best on a phone, with headphones on and the lights off. Tap &lt;strong&gt;Talk&lt;/strong&gt; and speak out loud. It works in English and Spanish.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href="https://deeptelar.github.io/scary-truc/#try=fBCcdzvoCAIArSpYtfY6DBqu24U85uGDFtbHKtvJMvD6cnlb5r0XTY8yC3rXOXvQ0BmSXnVzQExnxjmD9ZW-zD2YddvCLuwl5r6a9-Sibbsve4DRny76aDWcxxI79z0siht1p_QF5SatSLIlM-kvZm2gEF29X05-aOb1FBBBu9loA_4sTr3ieXpLtSScQfotPSvNCTmRvHNDnGssVbiUnFn2xsvt7l8yPddq9r6fHEETHhwmQ-ct5oBG6_pLcasBcOmItyWUmxMY1n2zkhlDUxCwQkooHLubXgk" rel="noopener noreferrer"&gt;Play it with Gemma here&lt;/a&gt;&lt;/strong&gt; plays with Gemma, no setup needed (the word is &lt;strong&gt;pumpkin&lt;/strong&gt;). It works until the credit I put on it runs out. Without it, the game still runs, but the caller only knows a few words. To get the real thing, add your own OpenRouter key or your own Ollama under &lt;strong&gt;Model&lt;/strong&gt; on the lock screen.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fraw.githubusercontent.com%2Fdeeptelar%2Fscary-truc%2Fmain%2Fdocs%2Fcall.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fraw.githubusercontent.com%2Fdeeptelar%2Fscary-truc%2Fmain%2Fdocs%2Fcall.gif" alt="A call in The Hedge: the phone unlocks, the player taps Talk and speaks, the caller stops to ask if there is a person on the phone, midnight turns the screen dark, the Hollow speaks on the line, and the caller is taken mid-word" width="390" height="844"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;That's one real call with Gemma as the caller and the Hollow, cut down to about a minute. Everything they say is what Gemma said. The player's lines in this recording are scripted, so that the filming could be repeated.&lt;/p&gt;

&lt;h2&gt;
  
  
  Code
&lt;/h2&gt;


&lt;div class="ltag-github-readme-tag"&gt;
  &lt;div class="readme-overview"&gt;
    &lt;h2&gt;
      &lt;img src="https://assets.dev.to/assets/github-logo-5a155e1f9a670af7944dd5e12375bc76ed542ea80224905ecaf878b9157cdefc.svg" alt="GitHub logo"&gt;
      &lt;a href="https://github.com/deeptelar" rel="noopener noreferrer"&gt;
        deeptelar
      &lt;/a&gt; / &lt;a href="https://github.com/deeptelar/scary-truc" rel="noopener noreferrer"&gt;
        scary-truc
      &lt;/a&gt;
    &lt;/h2&gt;
    &lt;h3&gt;
      A horror game played by phone: talk a terrified stranger out of a maze while something hunts by sound. Gemma 4 plays the caller, in your browser. Kotlin Multiplatform, built with Telar.
    &lt;/h3&gt;
  &lt;/div&gt;
  &lt;div class="ltag-github-body"&gt;
    
&lt;div id="readme" class="md"&gt;&lt;div class="markdown-heading"&gt;
&lt;h1 class="heading-element"&gt;Scary Truc&lt;/h1&gt;
&lt;/div&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;Don't trust the voice on the phone.&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;— scratched into a wall of The Tape&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;It's past midnight and your phone is ringing. &lt;strong&gt;Unknown number.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;You answer, and someone is whispering on the line, too fast, too close. They woke up somewhere
they shouldn't be. They don't know how they got there. The others who came with them are gone
and yours is the only number in that phone.&lt;/p&gt;
&lt;p&gt;You can't see where they are. You have their voice, a battery that is running out, and a notebook
where you draw the maze from what they tell you. The pencil fades.&lt;/p&gt;
&lt;p&gt;Something else is down there with them. It has no eyes. It finds you by how you sound. So you
talk them through it, &lt;em&gt;left, right, hide, don't breathe&lt;/em&gt;, and you keep them calm, because fear
is loud.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&lt;a href="https://deeptelar.github.io/scary-truc/" rel="nofollow noopener noreferrer"&gt;Answer the phone&lt;/a&gt;&lt;/strong&gt;. Headphones on, lights off…&lt;/p&gt;&lt;/div&gt;
  &lt;/div&gt;
  &lt;div class="gh-btn-container"&gt;&lt;a class="gh-btn" href="https://github.com/deeptelar/scary-truc" rel="noopener noreferrer"&gt;View on GitHub&lt;/a&gt;&lt;/div&gt;
&lt;/div&gt;


&lt;p&gt;About 6,500 lines of Kotlin, plus 1,800 lines of tests. It's one Compose Multiplatform codebase that builds the web page (Wasm) and a desktop game. The first commit is from October 27.&lt;/p&gt;

&lt;h2&gt;
  
  
  How I Built It
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;My first version wasn't scary.&lt;/strong&gt; It was a top-down maze where you threw treats and tricks to guide four little agents past certain death. It worked, and when I played it, it was boring, predictable and not scary at all. I added real-time pressure, a hunter, a siren at midnight, radio static. It got better and it still wasn't frightening, because I was looking at it from above, safe.&lt;/p&gt;

&lt;p&gt;So I threw it away. If the fear comes from not seeing, take the screen away. On day two the game became a phone call with one scared person on the line. The first time I played that version on my phone, just chatting with them was already scary, which all the sirens had never managed.&lt;/p&gt;

&lt;p&gt;(&lt;em&gt;Truc&lt;/em&gt; started as &lt;em&gt;trick&lt;/em&gt;, from trick-or-treat. In Catalan, &lt;em&gt;fer un truc&lt;/em&gt; also means to give someone a call. I kept the name.)&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The model: Gemma 4, playing three parts.&lt;/strong&gt; The hosted page uses Gemma 4 31B on OpenRouter, a few seconds a turn. The desktop build uses &lt;code&gt;gemma4:12b&lt;/code&gt; in a local Ollama. Gemma does three jobs in a call:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The caller's ears:&lt;/strong&gt; what you told them to do, and how it made them feel.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The caller's voice:&lt;/strong&gt; saying what happened as a frightened person would.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The Hollow:&lt;/strong&gt; the whispers on the line, made from what the caller fears most.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;The workflow: a graph that waits for you.&lt;/strong&gt; A call is a &lt;a href="https://github.com/deeptelar/telar" rel="noopener noreferrer"&gt;Telar&lt;/a&gt; graph. Telar is an open-source Kotlin library for AI workflows that I maintain. It existed before this challenge, and Scary Truc uses it from Maven Central like any other library.&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="nc"&gt;StateGraph&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Call&lt;/span&gt;&lt;span class="p"&gt;&amp;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;listen&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;node&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;LISTEN&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;work&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;call&lt;/span&gt; &lt;span class="p"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;ears&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;listen&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;call&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="n"&gt;call&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;heard&lt;/span&gt; &lt;span class="p"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;call&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;copy&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;heard&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;heard&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;walk&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;node&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;WALK&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;call&lt;/span&gt; &lt;span class="p"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;call&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;act&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;mazeOf&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;haunt&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;whisper&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;node&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;WHISPER&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;work&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;call&lt;/span&gt; &lt;span class="p"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;call&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;whispers&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;mazeOf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;call&lt;/span&gt;&lt;span class="p"&gt;)))&lt;/span&gt; &lt;span class="n"&gt;whisperer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;whisper&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;call&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="k"&gt;null&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;call&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;said&lt;/span&gt; &lt;span class="p"&gt;-&amp;gt;&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;said&lt;/span&gt; &lt;span class="p"&gt;==&lt;/span&gt; &lt;span class="k"&gt;null&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;call&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="n"&gt;call&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;whisper&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;said&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;speak&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;node&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;SPEAK&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;work&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;call&lt;/span&gt; &lt;span class="p"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;voice&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;speak&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;call&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="n"&gt;call&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;said&lt;/span&gt; &lt;span class="p"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;call&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;spoke&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;said&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="nc"&gt;START&lt;/span&gt; &lt;span class="n"&gt;then&lt;/span&gt; &lt;span class="n"&gt;speak&lt;/span&gt;
    &lt;span class="nf"&gt;conditionalEdge&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;speak&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;targets&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;setOf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;listen&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;NodeRef&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;END&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;call&lt;/span&gt; &lt;span class="p"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;call&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;over&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nc"&gt;NodeRef&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;END&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="n"&gt;listen&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="n"&gt;listen&lt;/span&gt; &lt;span class="n"&gt;then&lt;/span&gt; &lt;span class="n"&gt;walk&lt;/span&gt; &lt;span class="n"&gt;then&lt;/span&gt; &lt;span class="n"&gt;whisper&lt;/span&gt; &lt;span class="n"&gt;then&lt;/span&gt; &lt;span class="n"&gt;speak&lt;/span&gt;
&lt;span class="p"&gt;}.&lt;/span&gt;&lt;span class="nf"&gt;compile&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The run pauses before &lt;code&gt;listen&lt;/code&gt; on every turn, until you say something. The important node is the one with no model in it: &lt;strong&gt;&lt;code&gt;walk&lt;/code&gt; is plain code.&lt;/strong&gt; The caller obeys, refuses or bolts. The maze answers. The Hollow hunts. Only then does Gemma get a list of facts and say them out loud.&lt;/p&gt;

&lt;p&gt;That's the rule the whole game stands on: &lt;strong&gt;Gemma never decides what is true.&lt;/strong&gt; It decides how it sounds. So the caller can't invent a door that isn't there, and you can't talk them through a wall.&lt;/p&gt;

&lt;p&gt;The ears answer with a typed value, not text to parse. This class is the whole contract between what you say and the game:&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="kd"&gt;data class&lt;/span&gt; &lt;span class="nc"&gt;Understood&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="nd"&gt;@Description&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"What the stranger tells you to do now: ahead, left, right, back, stay, hide, mark, take, use, or none"&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;order&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="nd"&gt;@Description&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"How the stranger's words make you feel, from -2 (harsh, rushed, scary) to 2 (calm, kind, reassuring)"&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;calm&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;Int&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nd"&gt;@Description&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"When the stranger says to do something after that, the next thing ..."&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;then&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="s"&gt;"none"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nd"&gt;@Description&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Whether the stranger's words have nothing to do with getting you out alive: a joke, trivia ..."&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;offTopic&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;Boolean&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;false&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;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;understood&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;ask&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Understood&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;(&lt;/span&gt;&lt;span class="nf"&gt;prompt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;call&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;system&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;call&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;"Go right, you're doing great" gives &lt;code&gt;right&lt;/code&gt;, calm 2. A bare "left" needs no model at all, so it's instant when something is behind them.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The ghost in the machine.&lt;/strong&gt; The game keeps a log of each call on the phone, with what I said, what the caller understood, and how the call stood after each turn. I played, exported the log, and read what really happened. These six moments changed the game.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The caller lied to me.&lt;/strong&gt; In The Woods I said &lt;em&gt;"go right and pick up a stone if you can"&lt;/em&gt;. The caller answered &lt;em&gt;"I have the stone."&lt;/em&gt; There was no stone. Later, with the Hollow two steps away, I told them to throw it. &lt;em&gt;"I don't have any stones. It found me!"&lt;/em&gt; Dead. My graph had only run the "go right" part, so no fact said that nothing was picked up, and Gemma filled the silence with what I wanted to hear. A rule in the prompt forbade exactly that, and it wasn't enough. Now both parts of an order run, and what fails becomes a fact. When a model makes something up, first look for the fact you didn't give it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Buttons killed the conversation.&lt;/strong&gt; I had added tap options for one-handed play. Reading my own logs, my first four calls were nearly all taps and all ended badly, with the caller reciting menus at me. The call where I got someone out was mostly typed. It was also my third try at that maze, so the buttons weren't the only reason, but the point of the game is that it feels like a conversation. I removed them, and taught the caller to answer me first and report second.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;"Don't worry" was a cheat code.&lt;/strong&gt; My winning call took panic to zero by saying &lt;em&gt;no te preocupes&lt;/em&gt; on every turn. Nobody in that situation is at zero. Now the same comfort wears thin while all is quiet, and panic has a floor. My first attempt at this made The Ship impossible: my test guide, which plays perfectly, went from 12 of 12 to 0 of 12. It's 9 of 12 now, and 12 of 12 on the others.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Gemma was gone for a day and I didn't notice.&lt;/strong&gt; A regular expression that works on the JVM throws in the browser. Gemma failed silently, the game fell back to its script, and every test was green. Now model calls and faults go in the log, and I check model behavior in a real headless Chrome with a real key.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The best input was my own voice.&lt;/strong&gt; My last call was entirely by microphone, and it worked so well that I made the mic the main key: it's a phone call, so you should speak. It also gives the game its strangest moments. The speech recognition once heard me say a name I never said, and the caller, in the dark, with the Hollow awake, answered: &lt;em&gt;"I'm not Silvia. My name is Sam."&lt;/em&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A tester found what I couldn't see.&lt;/strong&gt; A real player liked it and told me three things. It got too easy, because there was plenty of time to think on every turn. The caller shouldn't explain that a breathing door is bad: say there's a door that breathes, and if someone dies going in, the player will understand. And trust, the main mechanic, was never tested. The log agreed: trust only ever rose, 60 to 87 in one call, and a 33-second pause cost nothing. So thinking now costs battery and nerve, and past a point the Hollow keeps walking. The notes stopped teaching the traps and started talking about the voice on the phone. And the caller now stops and asks who you are. Gemma judges your answer: &lt;em&gt;"I don't know, I'm scared too"&lt;/em&gt; convinces them, &lt;em&gt;"just go left"&lt;/em&gt; is no answer, and if you keep dodging, they hang up on you. It also remembers what you said. In the call in the recording above, the player said &lt;em&gt;"I'm staying with you"&lt;/em&gt;, and the Hollow answered:&lt;/li&gt;
&lt;/ul&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;Staying with you... and the weight of all they lost.&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Why Does Open Innovation Matter?
&lt;/h2&gt;

&lt;p&gt;Horror needs a character who stays in character. I can control that here because every part of it is mine to change: the prompts, the examples Gemma learns its voice from, the model itself.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;You can swap the model.&lt;/strong&gt; The caller speaks the OpenAI API, so any server works: Gemma 4 31B on OpenRouter, &lt;code&gt;gemma4:12b&lt;/code&gt; on my own 8 GB GPU, or a friend's proxy. Changing it is a field on the lock screen. I tuned the prompts by measuring what Gemma does, and I can keep doing that because the weights aren't going to change under me without warning.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;It doesn't depend on any one service.&lt;/strong&gt; If a provider disappears, the game keeps working on the next one, or on your own computer. With no model at all it still plays, with a simpler caller. The maze, the Hollow and the rules are code, so the game itself survives its model.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;It can run offline.&lt;/strong&gt; The desktop build talks to a local Ollama, so a whole call can stay on your machine. I checked instead of assuming: I traced every connection during full calls in English and Spanish on &lt;code&gt;gemma4:12b&lt;/code&gt;. With Ollama's cloud features switched off (&lt;code&gt;OLLAMA_NO_CLOUD=1&lt;/code&gt;), neither the game nor Ollama connected to anything outside my PC. On my 8 GB GPU, the first line takes up to a minute while the model loads, then it's 4 to 8 seconds a turn.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What you say stays yours, with one honest exception.&lt;/strong&gt; The API key stays in your browser and goes from there straight to the server you chose. The play log stays on the device until you export it, and never holds a key. The exception is the microphone: speech recognition is the browser's own, and most browsers run it on their maker's servers, so on Chrome your voice goes to Google. The desktop build has no microphone.&lt;/p&gt;

&lt;p&gt;I didn't run a closed model next to Gemma, so I can't say one would write a worse caller. What I can say is that an open model let me build this the way I wanted: a game anyone can run on their own machine, that outlives any API, with a character I can take apart and fix when it lies to me.&lt;/p&gt;

&lt;h2&gt;
  
  
  My Agent Session
&lt;/h2&gt;

&lt;p&gt;I built this with Claude Code. I had the ideas, made the design decisions, picked every sound, and tested it on my phone. Claude Code wrote most of the code, ran the measurements against Gemma, and drafted this post with me.&lt;/p&gt;

&lt;p&gt;The loop was simple, and it's the part I'd recommend: play a call, export the log, and hand it over with one line about what felt wrong. Then measure the fix against the real model before believing it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Prize Categories
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Best Use of Gemma&lt;/strong&gt;: Gemma 4 understands the player and voices both the caller and the Hollow, in English and Spanish, through a typed interface, on OpenRouter or locally in Ollama. The maze keeps it honest.&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>devchallenge</category>
      <category>hf26challenge</category>
      <category>gemma</category>
      <category>kotlin</category>
    </item>
    <item>
      <title>Peresós: a sloth that eats one item of my backlog a day, with Gemma 4 on my own GPU</title>
      <dc:creator>Manuel Alejandro Hermoso</dc:creator>
      <pubDate>Sun, 25 Oct 2026 14:42:04 +0000</pubDate>
      <link>https://dev.to/cuento3yllevo2/peresos-a-sloth-that-eats-one-item-of-my-backlog-a-day-with-gemma-4-on-my-own-gpu-j6a</link>
      <guid>https://dev.to/cuento3yllevo2/peresos-a-sloth-that-eats-one-item-of-my-backlog-a-day-with-gemma-4-on-my-own-gpu-j6a</guid>
      <description>&lt;p&gt;&lt;em&gt;This is a submission for the &lt;a href="https://dev.to/challenges/hacktoberfest-week3-2026-10-19"&gt;Hacktoberfest Open-Source AI Challenge Week 3: Slow and Steady&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What I Built
&lt;/h2&gt;

&lt;p&gt;I have a pile. Everyone has one: the message from the landlord I haven't answered, the gym membership I keep meaning to cancel, the friend asking whether Friday still works. None of these takes long. All of them need me to sit down, think for a minute and write something, and that minute is what I keep putting off.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Peresós&lt;/strong&gt; (Catalan for &lt;em&gt;sloth&lt;/em&gt;, and for &lt;em&gt;lazy&lt;/em&gt;) is a sloth that lives in my terminal and eats that pile, one item a day.&lt;/p&gt;

&lt;p&gt;I feed it whatever I'm avoiding:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;peresos feed &lt;span class="s2"&gt;"From my landlord: the yearly boiler check is due. The technician needs a morning next week when someone is home. Which day suits you?"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;It eats files too: &lt;code&gt;peresos feed bill.jpg&lt;/code&gt;, or right-click a photo or a PDF and &lt;strong&gt;Feed to Peresós&lt;/strong&gt;. Gemma looks at photos itself. Ollama takes no PDFs, so Peresós reads them on my computer and gives Gemma the text, or pictures of a scan's first pages.&lt;/p&gt;

&lt;p&gt;When I change my mind about an item, &lt;code&gt;peresos pile edit 3&lt;/code&gt; opens it in my editor, and &lt;code&gt;peresos pile remove 3&lt;/code&gt; takes it off. In the window, a pencil next to each item does the same.&lt;/p&gt;

&lt;p&gt;Once a day I wake it with &lt;code&gt;peresos&lt;/code&gt;. It takes the oldest item, and Gemma 4, running on my own machine, prepares the &lt;strong&gt;finished version&lt;/strong&gt;: the reply written as me, the decision with one sentence on why, the note cleaned up, or for a task, only the first five-minute step. Then it waits, and I do the only part that needs me:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;y&lt;/code&gt; take it: the finished version is saved, and the item leaves the pile for good&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;n&lt;/code&gt; bin it: deciding not to do something also finishes it&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;l&lt;/code&gt; later: back to the bottom of the pile&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;a&lt;/code&gt; another try: Gemma writes a different version, and knows which ones I didn't want&lt;/li&gt;
&lt;li&gt;Enter: decide another day. It keeps the item in its mouth until I come back&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The rules are what make it lazy in a good way:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;One item a day, at most.&lt;/strong&gt; It is a sloth. A pile of forty things is overwhelming; one thing, already done for you, is a five-second yes.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;It never nags.&lt;/strong&gt; Skip a week and it simply sleeps, then tells you how long, with some pride.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;On a lazy day, it is lazier than you.&lt;/strong&gt; &lt;code&gt;peresos --lazy&lt;/code&gt; (or &lt;em&gt;Something easy&lt;/em&gt; in the window) lets Gemma pick the easiest item on the pile and say why, then make the laziest version that still finishes it. Given a test pile with a tax return, a family trip, a gym membership and a friend asking about coffee, it picked the coffee: &lt;em&gt;"Just a tiny 'yes' is all it takes, no need to rush."&lt;/em&gt; The next lazy wake took the gym and answered &lt;em&gt;"Don't renew. At €45 per month for only 3 visits since June, the cost per visit is €15."&lt;/em&gt; Sometimes the laziest answer is not doing it, and it says so. The normal wake still eats the oldest item, so the tax return gets its turn.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;It writes a line in its diary after every meal&lt;/strong&gt;, in its own voice, and grows up as the pile shrinks: a baby that kicks its little legs, a young sloth chewing a leaf, a grown one yawning, and after ten items, a big round sloth asleep on its branch. Clear the whole pile and it climbs on top of its branch and sleeps there, full, until you feed it again.&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/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fny1t4fbwdjz2hesp7fju.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fny1t4fbwdjz2hesp7fju.gif" alt="The four phases of Peresós, each with its own animation, and each asleep on top of the branch after eating the whole pile" width="800" height="590"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;And it's yours to dress up: &lt;code&gt;peresos wear sunglasses coffee&lt;/code&gt; puts things on it, from sunglasses and a cap to a barretina (the red Catalan cap) or a steaming mug on the branch. Each item is a small drawing function, so adding your own is a good first pull request.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fejdeb8rlg3cp7zazogpd.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fejdeb8rlg3cp7zazogpd.gif" alt="Four sloths: a baby in sunglasses, a young one in a barretina with a coffee, a grown one in a cap, and a round one in sunglasses and a barretina" width="800" height="327"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;It also has a home. &lt;code&gt;peresos window&lt;/code&gt; puts it in the tray, like JetBrains Toolbox: a pixel face in the top bar that shows what it's doing, and a small window with a pixel-art jungle, today's bite and a menu for everything the terminal does. The terminal can close, and the desktop tells me when Gemma is done. The leaves follow the season and the sky follows the weather outside, from fog to snow on its branch. Click the sloth and it answers: a pat and it closes its eyes, a tickle and it laughs, a poke while it chews and it says &lt;em&gt;"Mmf. Mouth full."&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;An arrow in the title bar folds the window up to the sloth alone, so it can hang there while I work. Folded, the title bar says when a bite waits.&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%2Fb8y2mmrdom777jahrbwc.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%2Fb8y2mmrdom777jahrbwc.png" alt="Peresós in the tray: its face in the top bar with a yellow badge, and the window under it with a lazy bite waiting for an answer" width="414" height="656"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Getting it is one line, on Linux, macOS or Windows:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-fsSL&lt;/span&gt; https://raw.githubusercontent.com/deeptelar/peresos/main/install.sh | sh
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;(&lt;code&gt;irm .../install.ps1 | iex&lt;/code&gt; in PowerShell.) It brings its own Java, then &lt;code&gt;peresos doctor&lt;/code&gt; checks Ollama and Gemma and says what's missing. Where Ollama can't be installed, &lt;code&gt;peresos gemma off&lt;/code&gt; still brings one item a day, as I wrote it.&lt;/p&gt;

&lt;p&gt;Who gets to be lazier: anyone whose backlog is made of small things that each need a minute of thinking. The sloth does the minute. You do the yes.&lt;/p&gt;
&lt;h2&gt;
  
  
  Demo
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fe18z0fnowpadxuckzne3.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fe18z0fnowpadxuckzne3.gif" alt="A real session: one fact about me, three items fed to the pile, one wake answered with yes, and a second wake on the same day" width="800" height="592"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;That's a real session on Gemma 4 (12B) in Ollama, with the waiting cut short. First I tell it one fact about me, which it uses from then on: I work from home on Tuesdays and Thursdays. It turned the landlord's question into "Tuesday morning next week works well for me as I'll be at home", without me typing a day. The second &lt;code&gt;peresos&lt;/code&gt; on the same day gets "It already ate today. One item a day is plenty."&lt;/p&gt;
&lt;h2&gt;
  
  
  Code
&lt;/h2&gt;


&lt;div class="ltag-github-readme-tag"&gt;
  &lt;div class="readme-overview"&gt;
    &lt;h2&gt;
      &lt;img src="https://assets.dev.to/assets/github-logo-5a155e1f9a670af7944dd5e12375bc76ed542ea80224905ecaf878b9157cdefc.svg" alt="GitHub logo"&gt;
      &lt;a href="https://github.com/deeptelar" rel="noopener noreferrer"&gt;
        deeptelar
      &lt;/a&gt; / &lt;a href="https://github.com/deeptelar/peresos" rel="noopener noreferrer"&gt;
        peresos
      &lt;/a&gt;
    &lt;/h2&gt;
    &lt;h3&gt;
      A sloth in your terminal that eats one item of your backlog a day. Gemma 4 on your own machine prepares the finished version; you only say yes or no. Built on Telar.
    &lt;/h3&gt;
  &lt;/div&gt;
  &lt;div class="ltag-github-body"&gt;
    
&lt;div id="readme" class="md"&gt;&lt;div class="markdown-heading"&gt;
&lt;h1 class="heading-element"&gt;Peresós&lt;/h1&gt;
&lt;/div&gt;

&lt;p&gt;A sloth that eats one item of your backlog a day. You feed it what you keep putting off, and
&lt;strong&gt;Gemma 4, on your own machine&lt;/strong&gt;, prepares the finished version. You only say yes or no.&lt;/p&gt;
&lt;p&gt;&lt;a rel="noopener noreferrer" href="https://github.com/deeptelar/peresos/docs/peresos-demo.gif"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fraw.githubusercontent.com%2Fdeeptelar%2Fperesos%2FHEAD%2Fdocs%2Fperesos-demo.gif" alt="A session: tell it one fact, feed it three items, wake it, take the finished reply"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;em&gt;Peresós&lt;/em&gt; is Catalan for sloth, and for lazy.&lt;/p&gt;
&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;Install&lt;/h2&gt;
&lt;/div&gt;
&lt;div class="highlight highlight-source-shell notranslate position-relative overflow-auto js-code-highlight"&gt;
&lt;pre&gt;&lt;span class="pl-c"&gt;&lt;span class="pl-c"&gt;#&lt;/span&gt; Linux and macOS (Apple silicon)&lt;/span&gt;
curl -fsSL https://raw.githubusercontent.com/deeptelar/peresos/main/install.sh &lt;span class="pl-k"&gt;|&lt;/span&gt; sh&lt;/pre&gt;

&lt;/div&gt;
&lt;div class="highlight highlight-source-powershell notranslate position-relative overflow-auto js-code-highlight"&gt;
&lt;pre&gt;&lt;span class="pl-c"&gt;&lt;span class="pl-c"&gt;#&lt;/span&gt; Windows, in PowerShell&lt;/span&gt;
irm https:&lt;span class="pl-k"&gt;//&lt;/span&gt;&lt;span class="pl-c1"&gt;raw.githubusercontent.com&lt;/span&gt;&lt;span class="pl-k"&gt;/&lt;/span&gt;deeptelar&lt;span class="pl-k"&gt;/&lt;/span&gt;peresos&lt;span class="pl-k"&gt;/&lt;/span&gt;main&lt;span class="pl-k"&gt;/&lt;/span&gt;install.ps1 &lt;span class="pl-k"&gt;|&lt;/span&gt; iex&lt;/pre&gt;

&lt;/div&gt;
&lt;p&gt;It brings its own Java, the &lt;code&gt;peresos&lt;/code&gt; command, a menu entry and &lt;strong&gt;Feed to Peresós&lt;/strong&gt; in the
right-click menu of photos and PDFs. You also need &lt;a href="https://ollama.com/download" rel="nofollow noopener noreferrer"&gt;Ollama&lt;/a&gt;:&lt;/p&gt;
&lt;div class="highlight highlight-source-shell notranslate position-relative overflow-auto js-code-highlight"&gt;
&lt;pre&gt;peresos doctor --fix    &lt;span class="pl-c"&gt;&lt;span class="pl-c"&gt;#&lt;/span&gt; checks everything and pulls Gemma 4 (gemma4:12b, about 8 GB)&lt;/span&gt;&lt;/pre&gt;

&lt;/div&gt;
&lt;p&gt;&lt;code&gt;peresos update&lt;/code&gt; and &lt;code&gt;peresos uninstall&lt;/code&gt; do what they say. To build it yourself, see
&lt;a href="https://github.com/deeptelar/peresos/CONTRIBUTING.md" rel="noopener noreferrer"&gt;CONTRIBUTING.md&lt;/a&gt;.&lt;/p&gt;
&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;Use it&lt;/h2&gt;
&lt;/div&gt;
&lt;div class="highlight highlight-source-shell notranslate position-relative overflow-auto js-code-highlight"&gt;
&lt;pre&gt;peresos about &lt;span class="pl-s"&gt;&lt;span class="pl-pds"&gt;"&lt;/span&gt;I work from home on Tuesdays and Thursdays&lt;span class="pl-pds"&gt;"&lt;/span&gt;&lt;/span&gt;   &lt;span class="pl-c"&gt;&lt;span class="pl-c"&gt;#&lt;/span&gt; facts&lt;/span&gt;&lt;/pre&gt;…
&lt;/div&gt;&lt;/div&gt;
  &lt;/div&gt;
  &lt;div class="gh-btn-container"&gt;&lt;a class="gh-btn" href="https://github.com/deeptelar/peresos" rel="noopener noreferrer"&gt;View on GitHub&lt;/a&gt;&lt;/div&gt;
&lt;/div&gt;


&lt;p&gt;About 5,500 lines of Kotlin, plus 2,400 lines of tests. The tests run the whole workflow against a scripted model, so they need no GPU. On every release, GitHub Actions builds the installers for Linux, macOS and Windows, then installs, updates and uninstalls each one on a fresh machine. A soak test clicks the window's buttons at random through months of simulated days, while a terminal feeds and wakes the same sloth and Ollama stops halfway. It checks the pile, the checkpoint and the screen after every click, and it found four bugs in the window before I did.&lt;/p&gt;

&lt;h2&gt;
  
  
  How I Built It
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;The model: Gemma 4, in Ollama, on an 8 GB GPU&lt;/strong&gt; (an RTX 3070). I use &lt;code&gt;gemma4:12b&lt;/code&gt; through Ollama's OpenAI-compatible API. Gemma 4 thinks before it answers by default, and that turned out to be the most important knob:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;For preparing the finished version, I let it think. The replies are better, and the 10 to 60 seconds don't matter for something you were going to put off for a week anyway.&lt;/li&gt;
&lt;li&gt;For the diary line, I turn thinking off with &lt;code&gt;reasoning_effort: "none"&lt;/code&gt;. In my test, a three-word answer took 37 seconds with thinking and 4 without.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;The workflow: a small graph.&lt;/strong&gt; Peresós is built on &lt;a href="https://github.com/deeptelar/telar" rel="noopener noreferrer"&gt;Telar&lt;/a&gt;, an open-source Kotlin library for AI workflows that I maintain. To be clear about what's new: Telar existed before this challenge, and Peresós is a new app that uses it from Maven Central like any other library. One thing I did add to Telar this week, because the sloth needed it: images and PDFs in a model call (&lt;code&gt;Attachment&lt;/code&gt;, released in 0.1.0-alpha14). Feeding Gemma a photo is one more argument:&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="n"&gt;model&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ask&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Bite&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;(&lt;/span&gt;&lt;span class="nf"&gt;chewPrompt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sloth&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;plate&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;about&lt;/span&gt;&lt;span class="p"&gt;()),&lt;/span&gt; &lt;span class="n"&gt;system&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;CHEF&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;attachments&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;plate&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;attachments&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A wake is five nodes:&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="nc"&gt;START&lt;/span&gt; &lt;span class="n"&gt;then&lt;/span&gt; &lt;span class="n"&gt;wake&lt;/span&gt;
&lt;span class="nf"&gt;conditionalEdge&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;wake&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;targets&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;setOf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pick&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;chew&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;NodeRef&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;END&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;sloth&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;sloth&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;rest&lt;/span&gt; &lt;span class="p"&gt;!=&lt;/span&gt; &lt;span class="nc"&gt;Rest&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;NONE&lt;/span&gt; &lt;span class="p"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nc"&gt;NodeRef&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;END&lt;/span&gt; &lt;span class="c1"&gt;// already ate today, or nothing to eat&lt;/span&gt;
        &lt;span class="n"&gt;sloth&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;lazy&lt;/span&gt; &lt;span class="p"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;pick&lt;/span&gt;                     &lt;span class="c1"&gt;// a lazy wake: Gemma picks the easiest item first&lt;/span&gt;
        &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;chew&lt;/span&gt;                           &lt;span class="c1"&gt;// a normal wake eats the oldest&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="n"&gt;pick&lt;/span&gt; &lt;span class="n"&gt;then&lt;/span&gt; &lt;span class="n"&gt;chew&lt;/span&gt;
&lt;span class="n"&gt;chew&lt;/span&gt; &lt;span class="n"&gt;then&lt;/span&gt; &lt;span class="n"&gt;swallow&lt;/span&gt;
&lt;span class="nf"&gt;conditionalEdge&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;swallow&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;targets&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;setOf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;chew&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;diary&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;sloth&lt;/span&gt; &lt;span class="p"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sloth&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;bite&lt;/span&gt; &lt;span class="p"&gt;==&lt;/span&gt; &lt;span class="k"&gt;null&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;chew&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="n"&gt;diary&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="c1"&gt;// "another try" loops back&lt;/span&gt;
&lt;span class="n"&gt;diary&lt;/span&gt; &lt;span class="n"&gt;then&lt;/span&gt; &lt;span class="nc"&gt;NodeRef&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;END&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Three things in there carry the whole idea:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;The sloth is one data class, saved after every step.&lt;/strong&gt; When it last ate, what it's chewing, its meals, its diary: all in a &lt;code&gt;@Serializable&lt;/code&gt; &lt;code&gt;Sloth&lt;/code&gt;, written to a checkpoint file. Its memory from one day to the next is simply the checkpoint of the last run.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;It waits for me by stopping, not by blocking.&lt;/strong&gt; The graph pauses before &lt;code&gt;swallow&lt;/code&gt;. The process ends, the terminal can close, the computer can sleep. The next &lt;code&gt;peresos&lt;/code&gt; finds the paused run and resumes it with my answer, without asking Gemma again. For a sloth, a workflow that can wait a day for a human is not an edge case, it's the main feature.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Gemma answers with a typed value, not text I have to parse.&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;&lt;/li&gt;
&lt;/ol&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="kd"&gt;data class&lt;/span&gt; &lt;span class="nc"&gt;Bite&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
       &lt;span class="nd"&gt;@Description&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"REPLY for a message that waits for an answer, DECISION for a choice to make, NOTE for something to keep, TASK for something to do"&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;Kind&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
       &lt;span class="nd"&gt;@Description&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"A title of at most six words"&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;title&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="nd"&gt;@Description&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"The finished thing, ready to use as it is ..."&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;finished&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="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;bite&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;ask&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Bite&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;(&lt;/span&gt;&lt;span class="nf"&gt;chewPrompt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sloth&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;about&lt;/span&gt;&lt;span class="p"&gt;()),&lt;/span&gt; &lt;span class="n"&gt;system&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;CHEF&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;ask&lt;/code&gt; gives Gemma the JSON schema of the class and asks again, with the error, if an answer doesn't fit.&lt;/p&gt;

&lt;p&gt;One small detail I like: &lt;code&gt;swallow&lt;/code&gt; changes the pile and writes the finished file inside an &lt;code&gt;effect&lt;/code&gt;, which is saved with the step. If the computer dies halfway through, the sloth never eats the same item twice.&lt;/p&gt;

&lt;p&gt;The window runs the same graph through Telar's &lt;code&gt;GraphSession&lt;/code&gt;: the run is a &lt;code&gt;StateFlow&lt;/code&gt; of snapshots (running, waiting for my answer, done, stopped) that Compose draws, and the buttons call &lt;code&gt;start&lt;/code&gt; and &lt;code&gt;resume&lt;/code&gt;. The terminal and the window share one checkpoint, so they're two views of the same sloth.&lt;/p&gt;

&lt;p&gt;Three lessons from the first runs:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The sloth's voice leaked into my replies.&lt;/strong&gt; The first reply to a friend ended with "There is no need to hurry." Charming for a sloth, odd from me. Now preparing the finished version has its own instructions, "write as that person, never as an assistant", and only the diary sounds like the sloth.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A good model leaves blanks instead of guessing.&lt;/strong&gt; Asked for a day for the boiler check, Gemma wrote "Next [Day] morning". That was correct, since it didn't know my week, but it wasn't finished. So I added &lt;code&gt;peresos about&lt;/code&gt;: a few facts it uses every time, kept in a local file.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Other people's computers are the hard part.&lt;/strong&gt; The sloth took a day; installers for three systems took another. The first managed Windows PC still failed: its PowerShell blocked the .NET calls my installer used, so it now uses plain cmdlets, and CI tests that mode. That PC couldn't install Ollama either, which is where &lt;code&gt;peresos gemma off&lt;/code&gt; came from.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Why Does Open Innovation Matter?
&lt;/h2&gt;

&lt;p&gt;Look at what's on the pile: a message from a friend in Catalan, my landlord's email, how often I actually went to the gym, a photo of a bill. Together with the facts I gave it about myself, that's a small diary of my life. I would not paste it into a hosted API every day for the convenience of not writing a reply.&lt;/p&gt;

&lt;p&gt;With an open-weight model, I don't have to. Gemma 4 runs on my own GPU, and the pile, the facts, the diary and the checkpoints are plain files in &lt;code&gt;~/.peresos&lt;/code&gt;. Nothing leaves my machine, and there's no bill. (Only two things go out, and only when I ask: my town to Open-Meteo for the weather, and &lt;code&gt;peresos update&lt;/code&gt; to GitHub. Never the pile.) That also means a slow model is fine: the sloth can think as long as it likes, and nobody is counting tokens.&lt;/p&gt;

&lt;p&gt;The open pieces all the way down (Gemma, Ollama, an open-source workflow library) are why this could be a toy I trust with real messages, instead of a demo I'd only feed fake ones.&lt;/p&gt;

&lt;p&gt;And a personal opinion, since this is what the theme is about for me: open-source models like Gemma are going to redefine how we think about IoT and small devices, especially self-contained intelligence in offline environments, where privacy and security are key. Devices and programs with these capabilities, that we can interact with, will help people in their daily lives. This is the integration of AI into society that will benefit everyone.&lt;/p&gt;

&lt;h2&gt;
  
  
  Prize Categories
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Gemma&lt;/strong&gt;: Gemma 4 does all the work, locally in Ollama: typed answers for the finished versions, with thinking on, reading photos and scanned PDFs, and the diary with thinking off.&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>devchallenge</category>
      <category>hf26challenge</category>
      <category>hacktoberfest</category>
      <category>kotlin</category>
    </item>
    <item>
      <title>Hacktoberfest: 13 small, well-described issues in a Kotlin library for AI agents</title>
      <dc:creator>Manuel Alejandro Hermoso</dc:creator>
      <pubDate>Fri, 23 Oct 2026 05:46:17 +0000</pubDate>
      <link>https://dev.to/cuento3yllevo2/hacktoberfest-13-small-well-described-issues-in-a-kotlin-library-for-ai-agents-3b8c</link>
      <guid>https://dev.to/cuento3yllevo2/hacktoberfest-13-small-well-described-issues-in-a-kotlin-library-for-ai-agents-3b8c</guid>
      <description>&lt;p&gt;Hacktoberfest ends on October 31. If you write Kotlin, or want to, and are looking for a pull request that gets reviewed quickly, here are 13 open issues in &lt;a href="https://github.com/deeptelar/telar" rel="noopener noreferrer"&gt;Telar&lt;/a&gt;. Each one says what to build, which files to read first, and when it's done.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Telar is
&lt;/h2&gt;

&lt;p&gt;An open-source library (Apache 2.0) for AI agents and workflows in Kotlin Multiplatform. A workflow is a small graph: your own &lt;code&gt;data class&lt;/code&gt; is the state, each node is a &lt;code&gt;suspend&lt;/code&gt; function that returns a copy of it, and arrows decide what runs next. Around that there are checkpoints, pausing for a person's approval, retries, streaming, a tool-calling agent, MCP tools, and models from Claude, OpenAI, Gemini, Ollama, llama.cpp and LangChain4j. It runs on the JVM, Android, iOS, desktop and in the browser.&lt;/p&gt;

&lt;p&gt;It's an early alpha, written mostly by one person, which is why your pull request matters here and doesn't disappear in a queue.&lt;/p&gt;

&lt;h2&gt;
  
  
  What happens to your pull request
&lt;/h2&gt;

&lt;p&gt;The first fix from outside the project came in this week. It was reviewed and merged the same day, and the next morning it was on Maven Central, in &lt;a href="https://github.com/deeptelar/telar/releases/tag/v0.1.0-alpha13" rel="noopener noreferrer"&gt;&lt;code&gt;0.1.0-alpha13&lt;/code&gt;&lt;/a&gt;, with the contributor's name in the release notes.&lt;/p&gt;

&lt;p&gt;That's the deal for everyone:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;A review within a day or two&lt;/strong&gt;, with concrete comments, not "LGTM" and not silence.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Your name in the changelog and the release notes&lt;/strong&gt; of the version that ships your change.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;hacktoberfest-accepted&lt;/code&gt;&lt;/strong&gt; on every merged pull request, and the repository has the &lt;code&gt;hacktoberfest&lt;/code&gt; topic.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;CI runs on your pull request by itself&lt;/strong&gt;, so you see the result without waiting for anyone.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The issues
&lt;/h2&gt;

&lt;h3&gt;
  
  
  A sample: one evening, the public API only
&lt;/h3&gt;

&lt;p&gt;Each of these is a small runnable program, plus a test, that shows one feature. You read a guide, write code that uses the library, and the guide gets a "runnable version" link to yours. You don't need to know the internals.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://github.com/deeptelar/telar/issues/73" rel="noopener noreferrer"&gt;#73&lt;/a&gt; Go back in a run with &lt;code&gt;history&lt;/code&gt; and &lt;code&gt;fork&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/deeptelar/telar/issues/74" rel="noopener noreferrer"&gt;#74&lt;/a&gt; Merge the states of parallel nodes with &lt;code&gt;mergeRules&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/deeptelar/telar/issues/75" rel="noopener noreferrer"&gt;#75&lt;/a&gt; Report progress from inside a node&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/deeptelar/telar/issues/139" rel="noopener noreferrer"&gt;#139&lt;/a&gt; Watch a run with an &lt;code&gt;AgentObserver&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/deeptelar/telar/issues/140" rel="noopener noreferrer"&gt;#140&lt;/a&gt; Try a failing node again with &lt;code&gt;Retry&lt;/code&gt; and &lt;code&gt;timeout&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/deeptelar/telar/issues/141" rel="noopener noreferrer"&gt;#141&lt;/a&gt; Charge a card once with &lt;code&gt;effect&lt;/code&gt;, also when the step runs again&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;None of them needs an API key: the samples use a scripted model when there is none.&lt;/p&gt;

&lt;h3&gt;
  
  
  Small and outside the Kotlin code
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://github.com/deeptelar/telar/issues/77" rel="noopener noreferrer"&gt;#77&lt;/a&gt; Make the API reference link back to the documentation site&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/deeptelar/telar/issues/79" rel="noopener noreferrer"&gt;#79&lt;/a&gt; &lt;code&gt;telar-bom&lt;/code&gt;: a bill of materials, so an app names the version of Telar once (Gradle)&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/deeptelar/telar/issues/78" rel="noopener noreferrer"&gt;#78&lt;/a&gt; Run the agent sample against a provider you have a key for (Groq, OpenRouter, Mistral, DeepSeek, LM Studio, vLLM, ...) and report what works. A comment already helps; a pull request that adds the result to the guide also counts for Hacktoberfest.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Bigger, for when you want something to sink your teeth into
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://github.com/deeptelar/telar/issues/137" rel="noopener noreferrer"&gt;#137&lt;/a&gt; &lt;code&gt;telar-checkpoint-postgres&lt;/code&gt;: save runs in PostgreSQL&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/deeptelar/telar/issues/138" rel="noopener noreferrer"&gt;#138&lt;/a&gt; &lt;code&gt;telar-otel&lt;/code&gt;: a span for each call to a model and each tool call&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/deeptelar/telar/issues/80" rel="noopener noreferrer"&gt;#80&lt;/a&gt; A docs page for people who know LangGraph for Python&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/deeptelar/telar/issues/81" rel="noopener noreferrer"&gt;#81&lt;/a&gt; Run the JS and Wasm tests of the agent modules in a real browser&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  How to start
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Comment on the issue&lt;/strong&gt; you want, so two people don't write the same thing. I'll assign it to you.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Set up:&lt;/strong&gt; a JDK 17 or newer is all you need. Gradle, Kotlin and the rest come with the wrapper.
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;   git clone https://github.com/deeptelar/telar.git
   &lt;span class="nb"&gt;cd &lt;/span&gt;telar
   ./gradlew :samples:check   &lt;span class="c"&gt;# enough for a sample; ./gradlew check for everything&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Open the pull request&lt;/strong&gt; when the issue's "Done when" list is green. &lt;a href="https://github.com/deeptelar/telar/blob/main/CONTRIBUTING.md" rel="noopener noreferrer"&gt;CONTRIBUTING.md&lt;/a&gt; has the design rules, and questions are welcome on the issue itself.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Changes made with an AI assistant are fine, as long as you understand and have run what you submit.&lt;/p&gt;

&lt;h2&gt;
  
  
  Links
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Issues for a first contribution:&lt;/strong&gt; &lt;a href="https://github.com/deeptelar/telar/issues?q=is%3Aissue+is%3Aopen+label%3A%22good+first+issue%22" rel="noopener noreferrer"&gt;https://github.com/deeptelar/telar/issues?q=is%3Aissue+is%3Aopen+label%3A%22good+first+issue%22&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Code:&lt;/strong&gt; &lt;a href="https://github.com/deeptelar/telar" rel="noopener noreferrer"&gt;https://github.com/deeptelar/telar&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Docs and tutorial:&lt;/strong&gt; &lt;a href="https://deeptelar.github.io/telar/" rel="noopener noreferrer"&gt;https://deeptelar.github.io/telar/&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A game in the browser where every stage is a Telar graph:&lt;/strong&gt; &lt;a href="https://deeptelar.github.io/telar-demo/" rel="noopener noreferrer"&gt;https://deeptelar.github.io/telar-demo/&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you'd rather use it than build it, a star or an issue about what's missing helps too.&lt;/p&gt;

</description>
      <category>hacktoberfest</category>
      <category>kotlin</category>
      <category>opensource</category>
      <category>ai</category>
    </item>
    <item>
      <title>An AI agent that asks before it pays: human approval for tool calls in Kotlin</title>
      <dc:creator>Manuel Alejandro Hermoso</dc:creator>
      <pubDate>Tue, 13 Oct 2026 12:26:00 +0000</pubDate>
      <link>https://dev.to/cuento3yllevo2/an-ai-agent-that-asks-before-it-pays-human-approval-for-tool-calls-in-kotlin-5f63</link>
      <guid>https://dev.to/cuento3yllevo2/an-ai-agent-that-asks-before-it-pays-human-approval-for-tool-calls-in-kotlin-5f63</guid>
      <description>&lt;p&gt;Updated Oct 2026 for 0.1.0-alpha12: MCP tools, retries, tracing and a SQLite checkpointer are now in.&lt;/p&gt;

&lt;p&gt;An agent that only talks is easy to trust. An agent with a &lt;code&gt;refund&lt;/code&gt; tool is not. The model decides when to call it and with which amount, and the same question can get a different answer tomorrow.&lt;/p&gt;

&lt;p&gt;I don't think a better prompt solves that. "Never refund without permission" is a sentence the model usually follows, and "usually" is not good enough for money. What solves it is structure around the model: ordinary code that decides where the model may act alone and where it has to wait.&lt;/p&gt;

&lt;p&gt;This article builds that in Kotlin, in five steps: an agent, a pause before it acts, a pause that survives a restart, a test without a model, and a rule the model never gets to see. It uses &lt;a href="https://github.com/deeptelar/telar" rel="noopener noreferrer"&gt;Telar&lt;/a&gt;, an open-source library for agent workflows that I am writing. Every example here was compiled and run against the current release, &lt;code&gt;0.1.0-alpha12&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. The agent
&lt;/h2&gt;

&lt;p&gt;A tool is a function the model may call. Its input is a &lt;code&gt;@Serializable&lt;/code&gt; class, and the library builds the schema the model reads from that class.&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="kd"&gt;data class&lt;/span&gt; &lt;span class="nc"&gt;OrderLookup&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="nd"&gt;@Description&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"The order number, for example \"1001\""&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;orderId&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="nd"&gt;@Serializable&lt;/span&gt;
&lt;span class="kd"&gt;data class&lt;/span&gt; &lt;span class="nc"&gt;RefundRequest&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="nd"&gt;@Description&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"The order number, for example \"1001\""&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;orderId&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="nd"&gt;@Description&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"The amount to pay back, in euros"&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;euros&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;Int&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;fun&lt;/span&gt; &lt;span class="nf"&gt;helpDesk&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;ChatModel&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;pay&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;suspend&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;RefundRequest&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nc"&gt;Unit&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nc"&gt;CompiledGraph&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;AgentState&lt;/span&gt;&lt;span class="p"&gt;&amp;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;orderStatus&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Tool&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;OrderLookup&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;(&lt;/span&gt;&lt;span class="s"&gt;"order_status"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"Returns what was ordered, what it cost and when it arrived."&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;lookup&lt;/span&gt; &lt;span class="p"&gt;-&amp;gt;&lt;/span&gt;
        &lt;span class="s"&gt;"Order ${lookup.orderId}: one margherita for 9 euros, delivered 70 minutes late."&lt;/span&gt; &lt;span class="c1"&gt;// your database here&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;refund&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Tool&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;RefundRequest&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;(&lt;/span&gt;&lt;span class="s"&gt;"refund"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"Pays money back for an order."&lt;/span&gt;&lt;span class="p"&gt;)&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;-&amp;gt;&lt;/span&gt;
        &lt;span class="nf"&gt;pay&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="s"&gt;"Paid back ${request.euros} euros for order ${request.orderId}."&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;toolAgent&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;tools&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;listOf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;orderStatus&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;refund&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="n"&gt;system&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"You work at the help desk of a pizzeria. Look an order up before you decide anything about it."&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;&lt;code&gt;toolAgent&lt;/code&gt; is the usual loop: the model answers or asks for tools, the tools run, and their results go back to the model, until it has an answer.&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;model&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;AnthropicChatModel&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;HttpClient&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="nc"&gt;System&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getenv&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"ANTHROPIC_API_KEY"&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="s"&gt;"claude-opus-5-5"&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;agent&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;helpDesk&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;pay&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;payments&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;refund&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="n"&gt;orderId&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="n"&gt;euros&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;state&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;agent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;invoke&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;AgentState&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"My pizza for order 1001 came 70 minutes late. I want my money back."&lt;/span&gt;&lt;span class="p"&gt;)).&lt;/span&gt;&lt;span class="n"&gt;state&lt;/span&gt;
&lt;span class="nf"&gt;println&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;answer&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This works, and it pays whenever the model thinks it should. That is the part to fix.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Pause before the tools run
&lt;/h2&gt;

&lt;p&gt;The agent is not a black box. It is a graph of two nodes, &lt;code&gt;model&lt;/code&gt; and &lt;code&gt;tools&lt;/code&gt;, with an arrow back from one to the other. And a graph can be told to stop before a node:&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;config&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;GraphConfig&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;threadId&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"ticket-42"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;checkpointer&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;MemoryCheckpointer&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;AgentState&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;(),&lt;/span&gt;
    &lt;span class="n"&gt;interruptBefore&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;setOf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"tools"&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;Now &lt;code&gt;invoke&lt;/code&gt; returns &lt;code&gt;GraphResult.Interrupted&lt;/code&gt; each time the model has asked for tools and none of them has run yet. The calls that wait are in the state, with their names and their inputs:&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;suspend&lt;/span&gt; &lt;span class="k"&gt;fun&lt;/span&gt; &lt;span class="nf"&gt;answer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;agent&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;CompiledGraph&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;AgentState&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;,&lt;/span&gt;
    &lt;span class="n"&gt;question&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="n"&gt;config&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;GraphConfig&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;AgentState&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;,&lt;/span&gt;
    &lt;span class="n"&gt;approve&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;suspend&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;ToolCall&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;)&lt;/span&gt; &lt;span class="p"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nc"&gt;Boolean&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nc"&gt;AgentState&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;var&lt;/span&gt; &lt;span class="py"&gt;result&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;agent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;invoke&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;AgentState&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;question&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;config&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;while&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="k"&gt;is&lt;/span&gt; &lt;span class="nc"&gt;GraphResult&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Interrupted&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;refunds&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;messages&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;pendingToolCalls&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;filter&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="n"&gt;name&lt;/span&gt; &lt;span class="p"&gt;==&lt;/span&gt; &lt;span class="s"&gt;"refund"&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;refunds&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;isEmpty&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;||&lt;/span&gt; &lt;span class="nf"&gt;approve&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;refunds&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="c1"&gt;// Nothing risky, or a person said yes: run the calls.&lt;/span&gt;
            &lt;span class="n"&gt;agent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;resume&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;config&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="c1"&gt;// A person said no: answer the call yourself. A call that already has a result is not run.&lt;/span&gt;
            &lt;span class="n"&gt;agent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;resume&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;config&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;state&lt;/span&gt; &lt;span class="p"&gt;-&amp;gt;&lt;/span&gt;
                &lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;copy&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;messages&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;messages&lt;/span&gt; &lt;span class="p"&gt;+&lt;/span&gt; &lt;span class="n"&gt;refunds&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                    &lt;span class="nc"&gt;ChatMessage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;ToolResult&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="n"&gt;id&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="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"A person declined this refund."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;isError&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="p"&gt;})&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="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;state&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Three things happen in that loop:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Lookups pass without a question.&lt;/strong&gt; The code decides which tools need a person, by name. Nobody is asked to approve reading an order.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A yes runs the call as the model wrote it.&lt;/strong&gt; &lt;code&gt;approve&lt;/code&gt; gets the calls, so your screen can show &lt;code&gt;refund {"orderId":"1001","euros":9}&lt;/code&gt; before anyone clicks.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A no goes back to the model as a result.&lt;/strong&gt; The model reads "A person declined this refund." and writes the customer an answer that fits. The conversation stays whole.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The difference from a prompt is where the guarantee lives. The &lt;code&gt;refund&lt;/code&gt; function is not reached unless &lt;code&gt;approve&lt;/code&gt; returned true. The model cannot talk its way past an &lt;code&gt;if&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. The pause survives a restart
&lt;/h2&gt;

&lt;p&gt;A person may answer in a minute or after lunch, and the server may be deployed in between. Telar saves a checkpoint after every step, so the only change is where it is saved:&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;config&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;GraphConfig&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;threadId&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"ticket-42"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="c1"&gt;// One JSON file per thread. AgentState is @Serializable, so nothing else is needed.&lt;/span&gt;
    &lt;span class="n"&gt;checkpointer&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;FileCheckpointer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Path&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"checkpoints"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="nc"&gt;KotlinxStateSerializer&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;AgentState&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;()),&lt;/span&gt;
    &lt;span class="n"&gt;interruptBefore&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;setOf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"tools"&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;After a restart, &lt;code&gt;lastResult&lt;/code&gt; reads where a thread stopped without running anything:&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;when&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;result&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;agent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;lastResult&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;config&lt;/span&gt;&lt;span class="p"&gt;))&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;GraphResult&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Interrupted&lt;/span&gt; &lt;span class="p"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;showApproval&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;messages&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;pendingToolCalls&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt; &lt;span class="c1"&gt;// still waiting for a person&lt;/span&gt;
    &lt;span class="k"&gt;is&lt;/span&gt; &lt;span class="nc"&gt;GraphResult&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Completed&lt;/span&gt; &lt;span class="p"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;showAnswer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;answer&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;null&lt;/span&gt; &lt;span class="p"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;showEmptyForm&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="c1"&gt;// this ticket has never run&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;agent.resume(config)&lt;/code&gt; then continues in the new process, from the step where the old one stopped.&lt;/p&gt;

&lt;p&gt;One thing to know before you rely on this for payments: a tool call that finished is saved with the step, so resuming after a failure or a restart gives it its saved result instead of paying twice. A call that was cut off in the middle, by a crash during the payment itself, can run again. Give a side effect like a refund an idempotency key, for example made from the order number.&lt;/p&gt;

&lt;p&gt;There are checkpointers for memory, files, the browser's localStorage, and SQLite through SQLDelight on every platform. Checkpointer is a small interface, so a server database is one class away; PostgreSQL is &lt;a href="https://github.com/deeptelar/telar/issues/137" rel="noopener noreferrer"&gt;an open issue&lt;/a&gt; if you want to help.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. Test it without a model
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;ChatModel&lt;/code&gt; is an interface with one function, so a model that follows a script is a lambda. This one looks the order up, asks for the refund, and then answers according to what came back:&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;scripted&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;ChatModel&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;-&amp;gt;&lt;/span&gt;
    &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;results&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="n"&gt;messages&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;filterIsInstance&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;ChatMessage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;ToolResult&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;()&lt;/span&gt;
    &lt;span class="nc"&gt;ChatResponse&lt;/span&gt;&lt;span class="p"&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;results&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;size&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="p"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nc"&gt;ChatMessage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Assistant&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;toolCalls&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;listOf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;ToolCall&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"call-1"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"order_status"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;buildJsonObject&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nf"&gt;put&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"orderId"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"1001"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;})))&lt;/span&gt;
            &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="p"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nc"&gt;ChatMessage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Assistant&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;toolCalls&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;listOf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;ToolCall&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"call-2"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"refund"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;buildJsonObject&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nf"&gt;put&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"orderId"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"1001"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="nf"&gt;put&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"euros"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;9&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;})))&lt;/span&gt;
            &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nc"&gt;ChatMessage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Assistant&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;results&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;last&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="n"&gt;isError&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="s"&gt;"I am sorry, I cannot refund this order."&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="s"&gt;"Your 9 euros are on their way."&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="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And the property that matters becomes a unit test that runs in milliseconds, with no key and no network:&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;@Test&lt;/span&gt;
&lt;span class="k"&gt;fun&lt;/span&gt; &lt;span class="nf"&gt;aRefundThatAPersonDeclinesIsNotPaid&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;runTest&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;paid&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;mutableListOf&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;RefundRequest&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;()&lt;/span&gt;
    &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;agent&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;helpDesk&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;scripted&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;pay&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;paid&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="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;config&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;GraphConfig&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;checkpointer&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;MemoryCheckpointer&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;AgentState&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;(),&lt;/span&gt; &lt;span class="n"&gt;interruptBefore&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;setOf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"tools"&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;state&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;answer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;agent&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"My pizza for order 1001 came 70 minutes late. I want my money back."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;config&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;approve&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;false&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;

    &lt;span class="nf"&gt;assertEquals&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;emptyList&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="n"&gt;paid&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nf"&gt;assertEquals&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"I am sorry, I cannot refund this order."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;answer&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 model is the part you cannot predict. Everything around it is plain Kotlin on an immutable state, and you test it the way you test any other Kotlin.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. Rules go in code, not in the prompt
&lt;/h2&gt;

&lt;p&gt;Some rules have no exceptions. Say orders older than 30 days are never refunded. You could write that into the system prompt and hope. Or the model never sees those orders.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;toolLoop&lt;/code&gt; is the same agent as a part of a graph of your own, with the conversation in a state you define. &lt;code&gt;model&lt;/code&gt;, &lt;code&gt;orderStatus&lt;/code&gt; and &lt;code&gt;refund&lt;/code&gt; are the ones from step 1:&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;data class&lt;/span&gt; &lt;span class="nc"&gt;Complaint&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;orderId&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="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;text&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="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;daysAgo&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;Int&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;messages&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;ChatMessage&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;emptyList&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;reply&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="s"&gt;""&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;desk&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;StateGraph&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Complaint&lt;/span&gt;&lt;span class="p"&gt;&amp;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;tooOld&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;node&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"too_old"&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;copy&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;reply&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"Orders older than 30 days cannot be refunded."&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;send&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;node&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"send"&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;copy&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;reply&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="n"&gt;reply&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ifEmpty&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="n"&gt;messages&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;last&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="n"&gt;text&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;agent&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;toolLoop&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;tools&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;listOf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;orderStatus&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;refund&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="c1"&gt;// Where the conversation is in your state, and how to add messages to it.&lt;/span&gt;
        &lt;span class="n"&gt;messages&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="n"&gt;messages&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
        &lt;span class="n"&gt;append&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;complaint&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;new&lt;/span&gt; &lt;span class="p"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;complaint&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;copy&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;messages&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;complaint&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;messages&lt;/span&gt; &lt;span class="p"&gt;+&lt;/span&gt; &lt;span class="n"&gt;new&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
        &lt;span class="n"&gt;firstMessage&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="s"&gt;"Order ${it.orderId}: ${it.text}"&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
        &lt;span class="n"&gt;then&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;send&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="nf"&gt;conditionalEdge&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;START&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;targets&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;setOf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;agent&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;tooOld&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;if&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="n"&gt;daysAgo&lt;/span&gt; &lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;30&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;tooOld&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="n"&gt;agent&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="n"&gt;tooOld&lt;/span&gt; &lt;span class="n"&gt;then&lt;/span&gt; &lt;span class="n"&gt;send&lt;/span&gt;
    &lt;span class="n"&gt;send&lt;/span&gt; &lt;span class="n"&gt;then&lt;/span&gt; &lt;span class="nc"&gt;END&lt;/span&gt;
&lt;span class="p"&gt;}.&lt;/span&gt;&lt;span class="nf"&gt;compile&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;An old order takes the arrow to &lt;code&gt;too_old&lt;/code&gt;, and the model is not called at all. No tokens, no risk, and a test that proves it.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;compile()&lt;/code&gt; checks the drawing before anything runs. A node that nothing leads to, or an arrow to a node that does not exist, fails there and not in production at the fourth step of a run.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where this runs
&lt;/h2&gt;

&lt;p&gt;All of the above is Kotlin Multiplatform. The core depends only on kotlinx-coroutines, and the same graph runs on the JVM, Android, iOS, desktop, and in the browser as JS or Wasm. The models are Claude and anything with the API of OpenAI (OpenAI, Gemini, Ollama, Groq and others) on Ktor client, and any LangChain4j model on the JVM.&lt;/p&gt;

&lt;p&gt;To see graphs like these run without writing code, there is &lt;a href="https://deeptelar.github.io/telar-demo/" rel="noopener noreferrer"&gt;a small game in the browser&lt;/a&gt; in which every stage is a Telar graph. It needs no account and no API key.&lt;/p&gt;

&lt;h2&gt;
  
  
  What is missing
&lt;/h2&gt;

&lt;p&gt;Telar is an early alpha, and I would rather say so here than have you find out:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The API can still change before 1.0.&lt;/li&gt;
&lt;li&gt;A message holds text only: no images or files yet.&lt;/li&gt;
&lt;li&gt;A long conversation is sent to the model in full every time.&lt;/li&gt;
&lt;li&gt;Tracing has spans for runs and nodes, but not yet for each model call and tool call.&lt;/li&gt;
&lt;li&gt;The full list is on the &lt;a href="https://github.com/deeptelar/telar/blob/main/ROADMAP.md" rel="noopener noreferrer"&gt;roadmap&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;If you need a complete agent platform today, &lt;a href="https://github.com/JetBrains/koog" rel="noopener noreferrer"&gt;Koog&lt;/a&gt; from JetBrains is the safer choice. The site has &lt;a href="https://deeptelar.github.io/telar/why-telar" rel="noopener noreferrer"&gt;a comparison&lt;/a&gt; with Koog, LangGraph4j and LangChain4j.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Telar is for the case this article is about: the workflow around the model is the hard part, and you want it to be ordinary, typed, testable Kotlin.&lt;/p&gt;

&lt;h2&gt;
  
  
  Try it
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight kotlin"&gt;&lt;code&gt;&lt;span class="c1"&gt;// build.gradle.kts. `@Serializable` needs the plugin kotlin("plugin.serialization").&lt;/span&gt;
&lt;span class="nf"&gt;implementation&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"dev.deeptelar:telar-agent:0.1.0-alpha12"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nf"&gt;implementation&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"dev.deeptelar:telar-anthropic:0.1.0-alpha12"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;       &lt;span class="c1"&gt;// or telar-openai: OpenAI, Gemini, Ollama, ...&lt;/span&gt;
&lt;span class="nf"&gt;implementation&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"dev.deeptelar:telar-checkpoint-file:0.1.0-alpha12"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="c1"&gt;// step 3&lt;/span&gt;
&lt;span class="nf"&gt;implementation&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"dev.deeptelar:telar-serialization:0.1.0-alpha12"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;   &lt;span class="c1"&gt;// step 3&lt;/span&gt;
&lt;span class="nf"&gt;implementation&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"io.ktor:ktor-client-cio:3.6.0"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;                     &lt;span class="c1"&gt;// any Ktor engine&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Code:&lt;/strong&gt; &lt;a href="https://github.com/deeptelar/telar" rel="noopener noreferrer"&gt;https://github.com/deeptelar/telar&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Documentation and tutorial:&lt;/strong&gt; &lt;a href="https://deeptelar.github.io/telar/" rel="noopener noreferrer"&gt;https://deeptelar.github.io/telar/&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The guides behind this article:&lt;/strong&gt; &lt;a href="https://deeptelar.github.io/telar/guides/agents-with-tools" rel="noopener noreferrer"&gt;Agents with tools&lt;/a&gt; and &lt;a href="https://deeptelar.github.io/telar/guides/human-in-the-loop" rel="noopener noreferrer"&gt;Human-in-the-loop&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;It is early, which makes this the best time for feedback. If something in the API looks wrong to you, &lt;a href="https://github.com/deeptelar/telar/issues" rel="noopener noreferrer"&gt;open an issue&lt;/a&gt; or tell me in the comments.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>opensource</category>
      <category>kotlin</category>
      <category>android</category>
    </item>
  </channel>
</rss>
