Add Kindgi to an existing app
Kindgi doesn't need a project of its own. Run kindgi init in your app and
the pack lives beside your code: its tools import your app's modules, and
your app starts runs through the SDK.
A TypeScript or Node app
Section titled “A TypeScript or Node app”In the app's root (where its package.json is), with no pack name:
npx @kindgi/cli initpnpm install # or the app's own package managerinit adds:
- the pack's config: its id (from the app's name), version, and where its
primitives live. It's
kindgi.config.tsin an app whosepackage.jsonsays"type": "module", andkindgi.config.mtsin any other; - a
kindgi/folder for the primitives:kindgi/tools/,kindgi/agents/,kindgi/guardrails/,kindgi/flows/. In an app that isn't"type": "module", it also gets a one-linepackage.jsonthat makes them ES modules; @kindgi/sdk(a dependency) and@kindgi/cli(a devDependency) in yourpackage.json, at the CLI's own version, and, from 0.1.1,zod;- from 0.1.1, in a pnpm app,
allowBuilds: { esbuild: false }inpnpm-workspace.yaml: pnpm 11+ won't install until every dependency's install script has a decision, and esbuild (the CLI's bundler) doesn't need its script. A decision the app already has,trueorfalse, is kept; - the skills for your coding agent under
.claude/skills/, and.gitignoreentries (.kindgi/,.kindgirc.json,.env.local).
It creates no env files: kindgi dev reads your app's own .env and
.env.local. If your app lints with ESLint, leave out what Kindgi builds:
add ".kindgi/**" to the globalIgnores in eslint.config.mjs. To keep
the pack separate from the app instead, pass --new-repo.
Your code as a tool
Section titled “Your code as a tool”A tool imports your app's modules like any other file in it, path aliases
(@/…) included:
import { defineTool } from '@kindgi/sdk/define';import type { ToolId } from '@kindgi/sdk/types';import { z } from 'zod';
import { findRequest } from '@/lib/requests'; // your app's own code
const defined = defineTool({ id: 'acme-support.get-request' as ToolId, description: 'Looks up a support request by its id (REQ-1234).', version: '0.1.0', input: z.object({ id: z.string() }), output: z.object({ found: z.boolean(), subject: z.string().optional(), body: z.string().optional(), }), effects: [], mutating: false, handler: async ({ id }) => { const request = findRequest(id); return request ? { found: true, subject: request.subject, body: request.body } : { found: false }; },});
if (defined.kind === 'err') throw new Error(defined.error.message);export default defined.value;Ids start with the pack's id (acme-support, from the app's name).
mutating: false says the tool only reads, so a dry run may call it.
A package a tool imports (an ORM client such as @prisma/client, an API
SDK) must be in your app's dependencies, not devDependencies: the
deployed pack installs production dependencies only, so a dev-only import
works under kindgi dev and fails once deployed. kindgi dev warns when a
tool imports one, and kindgi build refuses the pack until it moves.
Build-time tools (the prisma CLI, typescript) stay in devDependencies.
An agent that uses it
Section titled “An agent that uses it”import { defineAgent } from '@kindgi/sdk/define';import type { AgentId, Semver } from '@kindgi/sdk/types';import { z } from 'zod';
const defined = defineAgent({ id: 'acme-support.triage' as AgentId, version: '0.1.0' as Semver, name: 'Triage', description: 'Reads a support request and sets its priority.', instructions: 'The user names a support request id. Look it up with `acme-support.get-request`, ' + 'then answer with its priority and a one-sentence summary.', capabilities: [{ needs: [{ feature: 'tool-use' as const }] }], tools: [{ id: 'acme-support.get-request', version: '^0.1.0' }], retrieval: [], guardrails: [], output: { schema: z.object({ priority: z.enum(['low', 'normal', 'urgent']), summary: z.string(), }), }, budget: { maxSteps: 4, maxCostUsd: 0.05, maxWallMs: 60_000 },});
if (defined.kind === 'err') throw new Error(defined.error.message);export default defined.value;output makes the answer typed: the model has to answer with JSON that
fits the schema, and your app gets it as an object.
Run it
Section titled “Run it”pnpm exec kindgi devkindgi dev builds the pack from kindgi/, runs its tools in a process
started from your app's root, and reloads on every save.
Until you register a model, agents answer with dev-echo, which only checks
the wiring: it calls the agent's first tool with {"message": …} and can't
produce a typed answer, so this agent fails with output-schema-violation.
Register a model first:
echo 'ANTHROPIC_API_KEY=sk-ant-…' >> .env.localpnpm exec kindgi providers register --preset=anthropicpnpm exec kindgi runs start --agent=acme-support.triage --input='{"userMessage":"Triage REQ-1002"}' "status": "completed",… "output": { "summary": "User unable to log in due to expired password with non-functional password reset email delivery.", "priority": "urgent" },What the pack's image needs
Section titled “What the pack's image needs”kindgi build turns the pack into an image. It installs your app's
production dependencies with install scripts off, so your app's own
postinstall and prepare scripts don't run there, and the build says so:
✓ The app's own install scripts don't run in the image: postinstall (`prisma generate`)When the image needs what such a script does, say so under image in
kindgi.config.ts.
Prisma. A tool that uses Prisma's client needs prisma generate in the
image. Without it the build fails when it loads the pack:
kindgi-index: file-import-failed: Failed to import kindgi/tools/find-order/index.ts: Error: @prisma/client did not initialize yet. Please run "prisma generate" and try to import it again.prisma() copies the schema into the image and runs prisma generate after
the install:
// in kindgi.config.tsimport { prisma } from '@kindgi/sdk/build';
const config = { // pack, discovery and environments, as kindgi init wrote them image: { extensions: [prisma({ schema: 'prisma/schema.prisma' })], },};With a prisma.config.ts, name it too:
prisma({ schema: 'prisma/schema.prisma', config: 'prisma.config.ts' }).
Also under image:
systemPackages: Debian packages the image needs, such as['tesseract-ocr'].buildEnv: placeholder variables for the build steps and for loading your modules while building, such as{ DATABASE_URL: 'postgresql://build-placeholder' }. Never secrets: they're set while building only, and the final image doesn't have them.- patch-package: when a skipped script runs it,
kindgi buildprints the build step to add (defineBuildExtension, also from@kindgi/sdk/build). With pnpm,pnpm patchapplies your patches in the install itself.
The build ends with the image and a check of what's in it:
✓ Built kindgi-pack/acme-orders:20261004.1 (sha256:042e285f8cd9…) ✓ /app/index.json in the image matches the local index byte for byteSelf-host with Docker pushes, signs and deploys it.
A Python app
Section titled “A Python app”In the app's directory (where its pyproject.toml is):
npx --yes @kindgi/cli@0.1 init # --pack-id=<id> if the app's name doesn't make oneuv sync # or what it prints for Poetry or pipnpx --yes @kindgi/cli@0.1 devinit edits your pyproject.toml in place, keeping its layout and
comments:
- it adds the
[tool.kindgi]tables: the pack id from[project].name, discovery underkindgi/, and, in a Poetry app,dev.pythonset to runpoetry run python; - it adds
kindgito[project].dependencies. Where it can't edit them (Poetry 1, ordynamicdependencies), it prints the command to run instead.
Then the kindgi/tools, guardrails, agents and flows folders, the
Python skills and .gitignore entries. An app with both a package.json
and a pyproject.toml gets a TypeScript pack unless you pass
--template=python.
The app's root is on sys.path, so a tool imports your packages by name:
from typing import Any
from pydantic import BaseModel
from acme.orders import find_orders # your app's own codefrom kindgi import tool
class Customer(BaseModel): customer_id: str
class Orders(BaseModel): orders: list[dict[str, Any]]
@tool(id="acme.customer-orders", mutating=False)def customer_orders(input: Customer) -> Orders: """The customer's orders, newest first.""" return Orders(orders=find_orders(input.customer_id))mutating=False says the tool only reads, so a dry run may call it.
A package a tool imports must be in your app's main dependencies
([project].dependencies), not a dev group: the deployed pack installs
without dev dependencies, so a dev-only import works under kindgi dev and
fails once deployed.
Inside kindgi/, import the pack's own modules relatively. Don't add an
__init__.py to kindgi/: the folder would shadow the kindgi package.
Starting runs from your app
Section titled “Starting runs from your app”Your app calls Kindgi over HTTP, through the SDK's client.
Connect your app to the runtime
Section titled “Connect your app to the runtime”In development, kindgi dev is enough: it writes the API's URL and a token
to .kindgirc.json, and the client finds them there, with a one-time warning
to set them. For production, and to silence the warning, give them to your
app as KINDGI_API_URL and KINDGI_API_TOKEN in your env file (.env /
.env.local). kindgi dev prints them (API and Token in its banner):
KINDGI_API_URL=http://127.0.0.1:4000KINDGI_API_TOKEN=kgi_bt_…The token stays the same when you restart kindgi dev. kindgi dev --reset
starts over with a new tenant and a new token: copy the new token after it
(the client warns when the token in your env file no longer matches).
In production they're your deployment's URL and API token. To pass them
yourself instead: createClient({ apiUrl, auth: { kind: 'apiToken', token } }),
or Kindgi(url, token=…) in Python.
In a CommonJS app
Section titled “In a CommonJS app”An app that isn't "type": "module", or that compiles to CommonJS, loads
the client with require():
const { createClient } = require('@kindgi/sdk/client');- Node 22.12 or later. The SDK is ES modules, and Node's
require()loads ES modules from 22.12 on. - TypeScript that compiles to CommonJS: TypeScript 5.8 or later, with
"module": "nodenext". In an app a bundler builds,"moduleResolution": "bundler"works too. With"moduleResolution": "node10"(or"node"), TypeScript doesn't find the SDK's types.
Pack code (kindgi/) is ES modules in every app; init sets that up.
Run an agent and read its answer
Section titled “Run an agent and read its answer”import { createClient } from '@kindgi/sdk/client';
export const kindgi = createClient(); // KINDGI_API_URL, KINDGI_API_TOKEN, or the running kindgi devimport { KindgiApiError } from '@kindgi/sdk/client';
import { kindgi } from '@/lib/kindgi';
type Triage = { priority: 'low' | 'normal' | 'urgent'; summary: string };
export async function GET(_request: Request, { params }: { params: Promise<{ id: string }> }) { const { id } = await params; try { const run = await kindgi.runs.start({ agent: 'acme-support.triage', input: { userMessage: `Triage ${id}` }, }); const { output } = run.output as { output: Triage }; // the agent's typed answer return Response.json(output); } catch (error) { if (error instanceof KindgiApiError) { const code = error.error.code === 'server' ? error.error.serverCode : error.error.code; return Response.json({ error: code, message: error.error.message }, { status: 502 }); } throw error; }}- An agent's input is
{ userMessage }.runs.startwaits for the answer. run.output.outputis the typed answer of an agent with anoutput;run.output.response.contentis the answer as text. A flow'srun.outputis the flow's own output.- When the turn fails, or the API refuses the call,
runs.startthrows aKindgiApiError.error.error.codesays what kind of failure it is (not-found,auth,network, …); for a failure on the server (server),error.error.serverCodeis the server's own code, such asoutput-schema-violation.
In Python:
from kindgi.client import Kindgi, KindgiApiError
kindgi = Kindgi() # KINDGI_API_URL, KINDGI_API_TOKEN, or the running kindgi devtry: run = kindgi.runs.start(agent="acme-support.triage", input={"userMessage": "Triage REQ-1002"}) triage = run.output["output"]except KindgiApiError as error: print(error.code, error.server_code, error.message)Follow a run as it runs
Section titled “Follow a run as it runs”Start the run with wait: false and the call returns as soon as the run
exists. Its events then arrive as they happen, through to its last one:
import { kindgi } from '@/lib/kindgi';
export async function GET(_request: Request, { params }: { params: Promise<{ id: string }> }) { const { id } = await params; const run = await kindgi.runs.start({ agent: 'acme-support.triage', input: { userMessage: `Triage ${id}` }, options: { wait: false }, // returns as soon as the run exists });
const encoder = new TextEncoder(); const body = new ReadableStream({ async start(controller) { for await (const event of kindgi.runs.stream(run.id)) { controller.enqueue(encoder.encode(`data: ${JSON.stringify(event)}\n\n`)); } controller.close(); }, }); return new Response(body, { headers: { 'content-type': 'text/event-stream' } });}Each event has a kind (run.started, run.step-started,
run.step-completed, …, run.completed) and a payload; the
run.completed event's payload.output is the run's output. In Python,
kindgi.runs.stream(run.id) is an iterator of the same events.
A browser can also follow a run directly, with a short-lived read-only token, without your API token: see Follow a run from the browser. To be told when a run ends instead, have Kindgi send your app a signed webhook: see Get a webhook when a run finishes.
Keep what a run did
Section titled “Keep what a run did”Store the run's id on your own row (a kindgi_run_id column), and read its
status, output, steps and sources through the API when your app shows them.
Never from Kindgi's database, and never by sending users to Kindgi's
console: see Show runs in your app.