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: sign in at
access.kindgi.com with GitHub for its pull
credentials, 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 directlynpx --yes @kindgi/cli@0.1runs the newest 0.1 release of the Kindgi CLI, the line these docs describe, without installing it (Check this machine says how).@0.1is optional:npx --yes @kindgi/cli init my-pack --template=scalaruns the newest release, which may be on a later line than these docs.init my-packmakes the pack in a new folder,my-pack.--template=scalamakes it a Scala pack, with examples.sbt -batch testruns the template's tests.-batchkeeps sbt from waiting for input.
init prints what it made, and sbt ends with the tests' summary:
…✓ Scala pack scaffolded at …/my-pack/ (preview)…[info] Passed: Total 3, Failed 0, Errors 0, Passed 3[success] Total time: 15 s, completed …Without 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 next commands find the runtime by
themselves. Leave it running.
When the pack is loaded and the runtime is up, it says so:
… ✓ loaded: 2 tools, 1 guardrails, 1 agents, 1 flows… ✓ Kindgi is up…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"}'--agent=my-pack.echo-agentand--flow=my-pack.echo-flowname what to run, by its id.--inputis the run's input, as JSON. An agent takes the person's message asuserMessage.
"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. It
calls the agent's first tool with {"message": <your userMessage>}, and
replies with what the tool 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 your AI model provider's API key as a secret, then register the provider:
./kindgiw secrets set <KEY_NAME> --env=local --scope=tenant./kindgiw providers register --preset=<preset><KEY_NAME> and <preset> depend on the provider:
| Provider | <KEY_NAME> |
<preset> |
|---|---|---|
| Anthropic | ANTHROPIC_API_KEY |
anthropic |
| OpenAI | OPENAI_API_KEY |
openai |
| Gemini (a Google AI Studio key) | GEMINI_API_KEY |
gemini-api |
| Groq | GROQ_API_KEY |
groq |
| OpenRouter (a hosted gateway) | OPENROUTER_API_KEY |
openrouter |
secrets set <KEY_NAME>asks for the key without showing it, and writes it to Kindgi's own.kindgi/secrets.env.--env=localis the environment the secret belongs to. Underkindgi dev,localis the pack's env files, that one among them.--scope=tenantis where the secret lives: the tenant, rather than one org (org:<orgId>) or project (project:<projectId>).--preset=<preset>registers the provider's models from Kindgi's built-in preset, which reads the<KEY_NAME>secret.kindgi providers presetslists every preset, Gemini on Vertex AI too.
With Anthropic's key, for example, the two commands print:
… Set ANTHROPIC_API_KEY at tenant in local.…✓ Registered anthropic: claude-opus-5-5, claude-sonnet-5-5 (default), claude-haiku-5-5, claude-haiku-4-5 — key ANTHROPIC_API_KEY (env local)The provider takes over from dev-echo at the next turn. 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": "<preset>"}]):
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.