Your state is a plain data class
Each node returns a copy of it, so a run is easy to test, print and save, and parallel branches cannot overwrite each other by accident.
Quick start
A model that calls your functions until it can answer. toolAgent is that loop, ready-made:
@Serializable
data class City(@Description("A city, for example \"Lisbon\"") val name: String)
// A tool: a function the model may call. Its input class becomes the schema the model reads.
val weather = Tool<City>("weather", "Returns today's weather in a city.") { city ->
"Light rain and 17 °C in ${city.name}." // call your real service here
}
suspend fun main() {
val model = AnthropicChatModel(HttpClient(), apiKey = System.getenv("ANTHROPIC_API_KEY"), model = "claude-opus-5-5")
val agent = toolAgent(model, tools = listOf(weather), system = "You are a travel assistant.")
// The agent is a graph like any other: stream it, save it, pause it before a tool runs.
agent.stream(AgentState("Do I need an umbrella in Lisbon today?")).collect { event ->
event.textDelta?.let { print(it) } // the answer, piece by piece
}
}The same agent with OpenAI, Gemini or a local Ollama model, a person who approves each tool call, and a chat that continues across turns: Agents with tools.
When an agent alone is not enough, draw the workflow yourself. Nodes are steps, then is an arrow, and a conditional edge picks the next step by looking at the state:
data class Refund(val orderId: String, val amount: Int, val approved: Boolean = false, val status: String = "new")
val refunds = StateGraph<Refund> {
val check = node("check") { it.copy(status = if (it.amount > 1000) "too large" else "checked") }
val pay = node("pay") { it.copy(status = if (it.approved) "paid" else "rejected") }
val decline = node("decline") { it.copy(status = "declined") }
START then check
conditionalEdge(check, targets = setOf(pay, decline)) { if (it.status == "checked") pay else decline }
pay then END
decline then END
}.compile() // a node nothing leads to, or a target that is not declared, fails here
suspend fun main() {
// Stop before money moves. The run is saved after every step.
val config = GraphConfig(threadId = "order-1001", checkpointer = MemoryCheckpointer<Refund>(), interruptBefore = setOf("pay"))
refunds.invoke(Refund("1001", amount = 250), config) // runs "check", then waits before "pay"
val result = refunds.resume(config) { it.copy(approved = true) } // a person said yes
println(result.state.status) // paid
}With FileCheckpointer in place of MemoryCheckpointer, the run waits in a file and continues after a restart: Human-in-the-loop.
// build.gradle.kts
implementation("dev.deeptelar:telar-core:0.1.0-alpha09") // graphs: all the workflow above needs
implementation("dev.deeptelar:telar-agent:0.1.0-alpha09") // chat models, tools and the agent
implementation("dev.deeptelar:telar-anthropic:0.1.0-alpha09") // Claude. Or telar-openai: OpenAI, Gemini, Ollama, Groq, ...Telar is in alpha: the API can still change before 1.0. Installation lists every module and its targets, and Why Telar compares it with Koog, LangGraph4j and LangChain4j.
| I want to | Go to |
|---|---|
| See a complete example with every line explained | Quick start |
| Learn graphs and agents from zero, in eight levels | Tutorial |
| Look up one feature | Guides |
| See graphs run before I write any code | Pixel Pizza, a game in the browser |
| Read what a function does | API reference |