Releases: Deep-CodeAI/Agents.KT
v0.2.2
A feature-heavy patch release — REPL deployment, multi-agent JAR composition (Swarm), four new observability hooks, two new budget controls, classpath-resource prompt loading, and a slimmer README. Pre-1.0 patch bump — no breaking changes; all existing API surface preserved.
Highlights
LiveShow / LiveRunner — REPL deployment surface mirroring MCP's two-layer split (LiveShow.from(x).start() / LiveRunner.serve(x, args)). Six factory overloads cover Agent / Pipeline / Forum / Parallel / Loop / Branch — any String-input structure becomes interactively chattable. ANSI color theme, full-resolution ASCII Agents.KT banner, in-place cat spinner during inference, lifecycle hooks (onTurnStart / onTurnEnd / onErrorReported), renderOutput post-processor, string-concatenated conversation history with --- user --- / --- assistant --- delimiters, slash commands (/quit, /clear, /help plus user-extensible slash(name) { }), --once "" for non-interactive single-turn use.
Swarm — multi-agent JAR composition. Drop sibling agent JARs into a folder, ServiceLoader-discover them, me.absorb(sibling) exposes each as a tool with full agent personality preserved (prompt, skills, knowledge, memory, observability hooks). In-JVM, no IPC overhead, no static-typing-across-JARs limitation MCP-stdio would impose. Captain-capable: any agent JAR can be elected by running its main.
Four new observability hooks. onError { Throwable } for infrastructure failures (LLM transport, parse, budget). Agent.observe { event } bridges the four legacy hooks into one sealed PipelineEvent stream. onBudgetThreshold(threshold) { reason, used } fires once per BudgetReason when cumulative usage crosses a fraction (pre-cap warning). LiveShow.onTurnStart / onTurnEnd / onErrorReported for REPL-side telemetry.
Two new budget controls. maxTokens (cumulative across turns when the provider reports usage; new BudgetReason.TOKENS) and maxConsecutiveSameTool (catches LLM retry loops on a broken tool; new BudgetReason.CONSECUTIVE_TOOL). LlmResponse.tokenUsage: TokenUsage? — Ollama's prompt_eval_count + eval_count plumbed through the agentic loop.
loadResource(path) for classpath prompts. prompt(loadResource("prompts/coder.md")) loads UTF-8 from the classpath; fail-fast at agent construction with a helpful error if the path is missing. loadResourceOrNull(path) for the optional case.
README split. Down from 1243 → 203 lines. Topical sections moved to docs/{skills, model-and-tools, mcp, error-recovery, memory, generation, composition, roadmap}.md with cross-back links.
Added
REPL / runtime
LiveShow.from(agent | pipeline | forum | parallel | loop | branch).start().runUntilTerminated() — programmatic REPL host. Six factory overloads collapse to one private constructor taking suspend (String) -> Any? (#981).
LiveRunner.serve(structure, args, configure) — picocli-shaped main shim mirroring McpRunner.serve. Six overloads, --once "", --max-history N, -h, -V. JVM shutdown hook + blocking until SIGTERM, returns int exit code (#981).
LiveShowBuilder configurables: prompt, maxHistoryTurns, historyDelimiter, input, output, plus UI polish: colors, theme, renderOutput, banner, spinner (#983).
LiveShowTheme.DEFAULT / LiveShowTheme.NONE color presets binding AnsiColor to roles (prompt / agentOutput / error / slashOutput / banner) (#983).
Spinner.CAT / Spinner.NONE — in-place cat-face spinner during inference, suppressed on non-TTY (#983).
Default banner — full-resolution ASCII rendering of the Agents.KT logo (angular cat face with pink crown accents, block-letter wordmark) (#983).
Swarm.discover() and Swarm.discover(classLoader) — ServiceLoader-walk for AgentProvider impls (#984).
interface AgentProvider { fun build(): Agent<*, > } — single-method SPI for sibling JARs (#984).
Agent<, >.absorb(sibling: Agent<, *>) — wraps the sibling as a tool on the captain; auto-enables across all skills; fails fast on name collision / typed-input siblings (#984).
Observability
Agent.onError { Throwable -> } — infrastructure-error observability hook (LLM transport, response parse, budget). Pure observability — original exception always rethrows; listener exceptions attached as suppressed (#962).
Agent.observe { event -> } — sealed PipelineEvent (SkillChosen / ToolCalled / KnowledgeLoaded / ErrorOccurred) bridges the four hooks into one typed stream; composes additively with prior listeners (#965).
Agent.onBudgetThreshold(threshold) { reason, usedPercent -> } — pre-cap warning hook; fires once per BudgetReason when cumulative usage crosses the fraction (#966).
Budget
BudgetConfig.maxTokens: Int? + BudgetReason.TOKENS — cumulative token cap; counts only when the provider reports tokenUsage on the response (#963).
BudgetConfig.maxConsecutiveSameTool: Int? + BudgetReason.CONSECUTIVE_TOOL — catches retry loops on a broken tool (#969).
LlmResponse.tokenUsage: TokenUsage? (promptTokens, completionTokens, total) — Ollama's prompt_eval_count + eval_count plumbed end-to-end (#963).
DX
loadResource(path: String): String — read agent prompts from src/main/resources/.... Fail-fast at agent construction; UTF-8 decoded; leading-slash normalized (#980).
loadResourceOrNull(path: String): String? — null-returning variant for optional resources (#980).
Agent.toString() — single-line Agent form replacing the JVM identity-hash default (#970).
Agent.describe(): String — multi-line debug summary of name + OUT type, prompt (truncated at 80), model config, budget (overrides only), skills, tools, memory bank presence (#970).
0.2.0
What's new
MCP
- HTTP / stdio / TCP transports, Bearer auth, namespaced tools, mock servers for tests
- McpServer.from(agent) with explicit tools/listChanged: false capability
- McpRunner standalone main
Typed tools
- tool<Args, Result>(...) with reflection-built JSON Schema
- @generable / @Guide annotations
- Sealed-args boundaries: rejected at the typed tool<> builder (separate untyped path remains)
Runtime hardening
- ForumTranscript deliberation pattern (transcriptCaptain)
- BranchRoute sealed type with onNull / onElse; sealed-completeness validation at construction
- SkillRoute(name, confidence, rationale) structured router output
- Untrusted tool-output wrapping — model can't impersonate framework messages
- Reserved tool names (memory_*) protected from shadowing
- Encapsulated toolMap / skills (read-only Map views; mutation only via DSL)
- Strict typed args — additionalProperties: false, sealed type discriminator must match constructed variant, repaired-args revalidation
Provider integration
- LlmProviderException — provider-boundary errors surface distinctly from output-parse errors
- Inline-tool fallback for Ollama models without native tool support
- Per-instance latch skips redundant native tool attempt after capability error observed
Suspend refactor
- invokeSuspend(input) on Agent + every composition operator
- executeAgentic and selectSkillByLlm are now suspend
- client.chat(...) wrapped in withContext(Dispatchers.IO) so cancellation interrupts HTTP I/O
- Parallel and Forum use coroutineScope for structured concurrency
Fixed
- Ollama provider error envelopes were silently passed through as LlmResponse.Text(rawJson), causing user transformOutput to fail with a misleading
"could not parse" error (#702) - Agent.mcp { } could mutate the tool registry post-construction because registerTool didn't checkNotFrozen() (#708)
- Agentic loop accepted repaired tool args without re-validating them through the typed schema (#658)
- constructFromMap accepted extra keys for plain data classes; sealed variants didn't verify the type discriminator matched (#665, #699)
- Tool name typos in tools(...) silently dropped instead of failing fast at construction (#631)
- Default budget was unbounded — agents could loop indefinitely without an explicit maxTurns (#633)
Migration
Existing code keeps working unchanged. For coroutine-scope callers, the new suspend entry points propagate cancellation cleanly:
runBlocking {
val result = myAgent.invokeSuspend("input") // no nested runBlocking
val out = (a then b).invokeSuspend("input") // suspend composition
val bounded = withTimeoutOrNull(2.seconds) { // works now
slowParallel.invokeSuspend("input")
}
}
No deprecations — the blocking shims are the documented back-compat surface.
0.1.1
Agents.KT v0.1.1 — Tool Error Recovery
Release date: 2026-03-29
The fixer is an agent.
What's new
Tool Error Recovery System
Every agent framework hits the same wall: tools fail at runtime. Malformed arguments, network errors, flaky APIs, type mismatches. The standard response is a dedicated parser class or a callback function. Agents.KT takes a different position: the fixer is an Agent<String, String> — same type system, same composition, same telemetry as everything else. Deterministic agents (implementedBy) cost zero LLM calls.
onError inside the tool block
Error handling lives where the tool lives:
tool("calculateNumberOfKeys") {
description("Count top-level keys in a JSON object")
executor { args ->
val json = args["json"]?.toString() ?: throw IllegalArgumentException("Missing json")
Regex(""""([^"]+)"\s*:""").findAll(json).count()
}
onError {
executionError { _ -> fix(agent = jsonFixer, retries = 2) }
invalidArgs { _, _ -> fix(agent = jsonFixer) }
}
}Three placement options with clear priority:
- Tool block
onError {}— highest priority - Agent-level
onToolError("name") {}— middle defaults { onError {} }— lowest, applies to all tools
The fixer is always an agent
No lambda callbacks. Repair uses Agent<String, String> — deterministic or LLM-driven:
// Deterministic — zero LLM calls
val jsonFixer = agent<String, String>("json-fixer") {
skills {
skill<String, String>("cleanup", "Fix JSON") {
implementedBy { input -> input.replace(",}", "}").replace(",]", "]") }
}
}
}
// LLM-driven — uses a model to analyze and fix
val smartFixer = agent<String, String>("smart-fixer") {
prompt("Fix malformed JSON. If structural error, call escalate.")
model { ollama("gpt-4o-mini"); temperature = 0.0 }
skills {
skill<String, String>("fix", "Analyze and fix JSON errors") {
tools("escalate")
}
}
}Built-in escalate and throwException tools
Every agent has two framework-provided tools registered at construction time — inactive by default, activated when a skill references them in tools(...).
escalate— soft failure. The error is fed back to the parent LLM as a tool result, giving it a chance to retry with corrected arguments. The fixer can include corrected data in the escalation reason.throwException— hard failure.ToolExecutionExceptionpropagates immediately. No retries.
// LLM-driven fixer calls escalate → error fed back → parent LLM retries
LLM calls parseJson(json = "{name: world}")
→ tool throws: "unquoted keys"
→ fixer invoked → fixer calls escalate("Corrected: {\"name\":\"world\"}")
→ error fed back to parent LLM
→ parent retries with corrected JSON → succeedsToolError sealed hierarchy
Four error types for programmatic handling:
sealed interface ToolError {
data class InvalidArgs(val rawArgs: String, val parseError: String, val expectedSchema: Map<String, Any?>)
data class DeserializationError(val rawValue: String, val targetType: KType, val cause: Throwable)
data class ExecutionError(val args: Map<String, Any?>, val cause: Throwable)
data class EscalationError(val source: String, val reason: String, val severity: Severity, val originalError: ToolError, val attempts: Int)
}
enum class Severity { LOW, MEDIUM, HIGH, CRITICAL }Tool Definition Block DSL
New ToolDefBuilder for richer tool definitions:
tools {
tool("fetch") {
description("Fetch a URL")
executor { args -> httpGet(args["url"].toString()) }
onError {
executionError { _ -> retry(maxAttempts = 3) }
}
}
}All existing tool("name", "description") { args -> ... } forms continue to work.
New files
| File | Purpose |
|---|---|
model/ToolError.kt |
ToolError sealed hierarchy, Severity, EscalationException, ToolExecutionException |
model/OnErrorBuilder.kt |
RepairResult, RepairScope, ToolErrorHandler, OnErrorBuilder, executeAgentFix |
Modified files
| File | Change |
|---|---|
model/ToolDef.kt |
ToolDefBuilder block DSL, ToolDefaultsBuilder, buildBuiltInTools() (escalate/throwException) |
model/AgenticLoop.kt |
executeToolWithRecovery() — error handler dispatch with retry, agent repair, escalation feedback |
core/Agent.kt |
onToolError(), getToolErrorHandler(), built-in tool auto-registration |
Tests
78 new tests across 10 test files:
| File | Tests | Coverage |
|---|---|---|
ToolErrorTest |
6 | Sealed hierarchy construction, exhaustive when |
OnErrorDSLTest |
10 | invalidArgs, deserializationError, executionError handlers |
ToolErrorDefaultsTest |
3 | Defaults apply to all tools, per-tool overrides |
ToolErrorAgentRepairTest |
4 | Agent-based fix, retries, escalation, throwException |
ToolErrorAgenticLoopTest |
6 | Retry recovery, retry exhaustion, escalation feedback, defaults in loop |
ToolLevelOnErrorTest |
16 | onError via onError= param, priority chain, agentic loop, escalation, throwException |
ToolBlockOnErrorTest |
9 | tool {} block DSL, priority over defaults/agent-level, agentic loop |
EscalateToolTest |
10 | Built-in tools in every agent, activation via tools(...), severity parsing |
JsonParseEscalationIntegrationTest |
3 | Full escalation flow: malformed JSON → fixer escalates → LLM retries → succeeds |
ThrowExceptionIntegrationTest |
5 | Hard failure: throwException kills pipeline, doesn't fire onToolUse, ignores remaining retries |
Integration tests (live LLM via Ollama):
- Flaky tool retry recovery with real LLM
- Retry exhaustion →
ToolExecutionException - Escalation → LLM reads corrected data from error → retries → succeeds
- Agent-based repair with real LLM
- Defaults across multiple tools with real LLM
- Tool block
onErrorwith escalation and real LLM throwExceptionstops pipeline with real LLM
Breaking changes
None. All existing APIs and tests continue to work unchanged.
Upgrade
// build.gradle.kts
dependencies {
implementation("ai.deep-code:agents-kt:0.1.1")
}Agents.KT — Define Freely. Compose Strictly. Ship Reliably.
0.1.0
First version good enough to make strict agent pipelines