Optimization Built for Production.

Eignex builds COMBO, a Kotlin library for optimizing agents under hard constraints. Pick the model, temperature, prompt style, and tools per request. Learn from how each run scored. Never violate the rules you declared.

View COMBO on GitHub

What COMBO does

You declare a decision space: typed variables (model, temperature, prompt style, tools) plus the rules they have to obey.

No top-tier model on the free plan. Code execution requires a sandbox tool. Casual tasks stay under a token cap.

Every configuration COMBO hands back is one that satisfies every rule, by construction.

Use it two ways. Embed the Kotlin multiplatform library directly in your service. Or deploy COMBO as its own pod and call into it from any language through the SDKs. Same decision space, same loop, same guarantees.

COMBO optimization loop A three-step cycle: choose a configuration, serve it to the request, update the optimizer with the observed reward. 01 choose 02 serve 03 update learn, then pick again

What COMBO picks from

Variables and constraints live in a single Kotlin class. Each variable carries a type (boolean, integer, float, nominal, or a multi-select set) and each constraint is a plain logical expression over those variables and the per-request context. The compiler hands the result to a constraint solver that knows how to sample feasible configurations from it. Every choose call returns one of those configurations.

class AgentPolicy : DecisionSpace() {
    val taskType    by contextNominal("coding", "writing", "research", "casual")
    val userTier    by contextNominal("free", "pro", "enterprise")

    val model       by nominal("haiku", "sonnet", "opus")
    val temperature by floatVar(min = 0.0, max = 1.0, buckets = 16)
    val promptStyle by nominal("terse", "detailed", "chainOfThought")
    val tools       by multiple("web_search", "code_exec", "file_read", "bash")

    val freeTierCantUseOpus by constraint { (userTier eq "free") implies (model ne "opus") }
    val codeExecNeedsBash   by constraint { tools.contains("code_exec") implies tools.contains("bash") }
    val casualKeepsItCheap  by constraint { (taskType eq "casual") implies (model ne "opus") }
}
name: AgentPolicy

context:
  taskType: { type: nominal, labels: [coding, writing, research, casual] }
  userTier: { type: nominal, labels: [free, pro, enterprise] }

variables:
  model:       { type: nominal,  labels: [haiku, sonnet, opus] }
  temperature: { type: float,    min: 0.0, max: 1.0, buckets: 16 }
  promptStyle: { type: nominal,  labels: [terse, detailed, chainOfThought] }
  tools:       { type: multiple, labels: [web_search, code_exec, file_read, bash] }

constraints:
  freeTierCantUseOpus:
    type: imp
    left:  { type: nomeq, name: userTier, label: free }
    right: { type: not, child: { type: nomeq, name: model, label: opus } }

  codeExecNeedsBash:
    type: imp
    left:  { type: ref, name: tools.code_exec }
    right: { type: ref, name: tools.bash }

  casualKeepsItCheap:
    type: imp
    left:  { type: nomeq, name: taskType, label: casual }
    right: { type: not, child: { type: nomeq, name: model, label: opus } }

Handing the choice to the request

COMBO covers two regimes from one decision space. Use it online for high-volume traffic where every request is a new draw and you care about cumulative reward over millions of decisions. Use it offline for expensive sweeps where each evaluation costs minutes or money and you have a budget of tens to hundreds of trials. Same schema, same rules, a different optimizer plugged in behind the loop.

Either way the call shape is the same. Ask for a configuration, hand it to the request that needed it, then wait for the outcome. A null return is the rare honest answer that no configuration satisfies the rules, not a quiet fallback.

val choice = combo.choose() ?: return  // null only if no feasible config exists
serve(choice)                          // your code: run the request with this configuration
combo.update(choice, observe())        // score the run, learn for next time
# null exit only if no feasible config exists
choice=$(curl -fsS -X POST "$COMBO_URL/v1/choose" \
  -H 'content-type: application/json' \
  -d '{"context": {"taskType": "coding", "userTier": "pro"}}') || exit

# your code: run the request with this configuration
serve "$choice"

# score the run, learn for next time
curl -fsS -X POST "$COMBO_URL/v1/update" \
  -H 'content-type: application/json' \
  -d "{\"choice\": $choice, \"reward\": $(observe)}"

Learning from each result

Every observed outcome feeds back into the model. Updates are safe to apply from many threads at once, with the locking strategy picked per statistic so hot paths stay fast. Choose returns in milliseconds whether embedded or behind an SDK call.

Each pod learns on its own and ships what it learned to the others. Run as many copies as you need. They converge to the same answer as a single instance would, with nothing central to coordinate them on the request path.

Read the README All projects on GitHub

Recent Posts

Agentic Coding Has No Floor

Opinions Rasmus Ros 9 min read

Vibe coding is what agentic coding decays into when you're tired or four hours in. The structural fix has to come from the harness vendors, not from another instruction file.

One Shape Across the Eignex Stack

Updates Rasmus Ros 4 min read

Three months into the Eignex rewrite, the libraries finally share one config shape that doubles as a YAML wire format. A checkpoint on what changed in each repo.

From Stringly to Strongly Typed

Engineering Rasmus Ros 7 min read

Three attempts at typed schemas in Kotlin: an imperative builder, a type-encoded product, and the property-delegate design I ended up shipping as skema.

Writing the Loss Function

Opinions Rasmus Ros 7 min read

AI plus a feed isn't a new medium, it's the same engagement loop with cheaper supply. The objective the loop optimizes for is a choice, not a law of physics.

KEncode: Packing Data for Strict Limits

Engineering Rasmus Ros 14 min read

Sometimes 80 characters of URL is all you get, and JSON won't survive the trip. kencode squeezes structured state through it, with the schema written as a plain Kotlin data class.

Engine Building and Status Updates

Updates Rasmus Ros 2 min read

What I'm actually trying to build: a continuous optimizer that learns from a live stream, fits a probabilistic model, and leans on an SMT solver to stay inside hard constraints.

Building Eignex in the Open

Updates Rasmus Ros 2 min read

A quick who-am-I and what-is-this. PhD in continuous optimization, left academia, now building Eignex in the open.

View All Posts →