Quickstart: Scala
The same pack as the TypeScript quickstart, in Scala: two tools, an agent that calls them, a guardrail and a flow. Its tools are case classes and functions, built with sbt, on Scala 3 (2.13 works too).
Before you start, set up what the Install page describes:
Docker, Node 22.12 or later, and access to the runtime image. The image is
in private preview: request access at contact@kindgi.com, then log in once
with kindgi auth registry. You also need a JDK 17 or later, with
JAVA_HOME set to it, and sbt 1.10 or
later.
1. Create the pack
Section titled “1. Create the pack”npx --yes @kindgi/cli@0.1 init my-pack --template=scalacd my-packsbt -batch test # the template's tests: the tools, called directlyWithout Node, run init from PyPI instead:
uvx --from "kindgi-cli>=0.1,<0.2" kindgi init my-pack --template=scala.
The pack is an sbt project. Its config is kindgi.config.json; its tools,
guardrails, agents and flows are objects in four packages:
my-pack/├── kindgi.config.json # "language": "scala", the pack's id and version├── build.sbt, project/build.properties # kindgi-pack-scala, Scala 3.3, Java 17├── kindgiw, kindgiw.cmd # the Kindgi CLI wrapper (the version kindgi.config.json pins)├── src/main/scala/mypack/│ ├── tools/Echo.scala, tools/Greet.scala # Tool[Input, Output](...)│ ├── guardrails/ResponseNotEmpty.scala # Guardrail[Config](...)│ ├── agents/EchoAgent.scala # Agent(...)│ └── flows/EchoFlow.scala # Flow(...)└── src/test/scala/mypack/ToolsSuite.scalaThe package comes from the pack's id (my-pack → mypack). A tool, a
guardrail, an agent or a flow is a val of an object named like its file:
Echo.scala holds object Echo.
The pack pins the Kindgi CLI it runs with, "cli" in kindgi.config.json.
Run the CLI as ./kindgiw (kindgiw.cmd on Windows): it uses Node's npx
when Node is installed, else uvx (no Node needed). kindgi upgrade moves
the pin, and kindgi-pack-scala's version in build.sbt with it.
2. Run it
Section titled “2. Run it”./kindgiw devkindgi dev checks the JDK (JAVA_HOME's) and sbt, and starts the Kindgi
runtime in Docker. sbt compiles the pack through its server (sbt --client), kindgi-pack's indexer reads it, and its tools and checks run in
a pack service. The first build starts the server and loads the build;
after that, a save compiles in about a second. kindgi dev starts that
server when none is running and stops it when it stops; a server you
already run (your IDE's) is used and left running. It writes the API's URL
and a token to .kindgirc.json, so the commands below find the runtime by
themselves. Leave it running.
3. Run the agent and the flow
Section titled “3. Run the agent and the flow”In a second terminal, in my-pack:
./kindgiw runs start --agent=my-pack.echo-agent --input='{"userMessage":"Ada"}'./kindgiw runs start --flow=my-pack.echo-flow --input='{"message":"Ada"}' "status": "completed",…⚠ dev-echo answered, and it isn't a real model: it only repeats what it's given. …… "echo": "Ada",Without a model, the agent's answer comes from dev-echo, a stand-in that
calls the agent's first tool with {"message": <your userMessage>} and
replies with what it returned, after a first line that says it isn't a real
model. The flow runs the echo tool on its input and returns what the tool
returned. Either way, your Scala tool ran: the runtime called it over HTTP in
the pack service.
4. Look at the code
Section titled “4. Look at the code”A tool is a val: its input and output are case classes, and their schemas
come from them. Option is optional, a parameter's default is the schema's
default, and a Jakarta Validation constraint becomes its schema keyword. The
pack service checks both on every call:
package mypack.tools
import com.kindgi.pack.scaladsl._import jakarta.validation.constraints.{PositiveOrZero, Size}import java.time.Instant
/** my-pack.echo — echoes the message back with a UTC timestamp and its length. */object Echo { final case class Input(@Size(min = 1, max = 500) message: String) final case class Output(echo: String, echoedAt: Instant, @PositiveOrZero characterCount: Int)
val tool: Tool[Input, Output] = Tool[Input, Output]("my-pack.echo") .description("Echoes the caller-provided message with a UTC timestamp and character count.") .readOnly .handler((in, _) => Output(in.message, Instant.now(), in.message.length))}readOnly says the tool only reads. A dry run may call it, and approval
gates skip it. Left out, a tool counts as one that writes. A tool built on
futures uses handlerAsync, which takes a Future.
An agent is data, and it refers to the tools and the guardrail themselves:
// in src/main/scala/mypack/agents/EchoAgent.scalaval agent: Agent = Agent("my-pack.echo-agent") .version("0.1.0") .name("Echo Agent") .instructions( "For each user message: if the user sends a name, greet them with the greet tool. " + "Otherwise echo their message with the echo tool. Quote the tool result verbatim.") .capability(Map("needs" -> List(Map("feature" -> "tool-use")))) .tool(Echo.tool) .tool(Greet.tool) .guardrail(ResponseNotEmpty.guardrail) .build()Save a file with a mistake in it, and kindgi dev says where, keeping the
last good code running:
✗ refresh failed [bundle-failed] The pack's code did not build: src/main/scala/mypack/tools/Greet.scala:16:31: Type Mismatch Error: Found: (42 : Int); Required: StringFix it, and the pack reloads:
✓ loaded 5 primitives (2 tools, 1 guardrails, 1 agents, 1 flows) in 1093msKnown limits
Section titled “Known limits”- Scala's primitives inside a type parameter (
Seq[Int],Option[Long]) areObjecton the JVM, so the schema allows any JSON value there. Use a case class (Seq[Item]), or give the schema yourself withTool.json. - In Scala 3, name a Java annotation's arguments:
@Min(value = 0), not@Min(0). Scala 3 passes a positional argument to the wrong element. - A Scala 3 enum whose cases take parameters is a sealed hierarchy, not a
string: describe it with
@JsonTypeInfoand@JsonSubTypes. A simple enum (enum Color { case Red, Green }) is a string of its case names. - jackson-module-scala must match your app's
jackson-databindminor version, and refuses another. If your app uses a newer Jackson, depend on the matchingjackson-module-scalayourself. - A tool defined as a
defor alazy valisn't read without running it: the indexer says so, and asks for aval.
5. Connect a real model
Section titled “5. Connect a real model”Store an Anthropic key as a secret (you're prompted for it; it isn't echoed), then register the provider:
./kindgiw secrets set ANTHROPIC_API_KEY --env=local --scope=tenant./kindgiw providers register --preset=anthropicIt takes over from dev-echo at the next turn. OpenAI (OPENAI_API_KEY,
--preset=openai), Gemini (GEMINI_API_KEY, --preset=gemini-api), Groq
(GROQ_API_KEY, --preset=groq) and OpenRouter (OPENROUTER_API_KEY,
--preset=openrouter) work the same way; kindgi providers presets lists
them, Gemini on Vertex AI too. Any OpenAI-compatible endpoint registers from
a short spec file.
The registration is in this project's dev database. To have kindgi dev
register the model on every boot, in each worktree and after --reset,
declare it in kindgi.config.json ("providers": [{"preset": "anthropic"}]):
Declare them in your pack's config.
- Add Kindgi to an existing Scala app: your app's own classes as tools.
- Guides: one task at a time.
- Concepts: packs, runs and the journal, security.
- kindgi-pack-scala's README: schemas from Scala's types, guardrails, flows, futures and the limits.