Skip to content

Give a tool per-project values

View as Markdown

This guide gives a tool values that differ per project, such as a base URL or a region, and reads them in the tool's code.

  • A pack, on a runtime such as kindgi dev, with Kindgi 0.1's CLI. This guide uses the quickstart's pack, my-pack.
  • A project's id, to set a value for one project. kindgi projects list lists the projects with their ids.

Some of what a tool needs differs from one customer or project to the next, without being secret: a base URL, an account or region id, a queue name. A tool declares these env values by name. You set them for the whole tenant, an org or a single project. On every call, Kindgi hands the tool the most specific value in the call's context.

Ask your coding agent: "I want a tool that looks up an order in our order system. Its address and region differ per project." Kindgi's skills in your pack tell it how (How it knows Kindgi). For reference, the tool looks like this:

tools/find-order/index.ts
import { defineTool } from '@kindgi/sdk/define';
import type { ToolId } from '@kindgi/sdk/types';
import { z } from 'zod';
const defined = defineTool({
id: 'my-pack.find-order' as ToolId,
description: 'Finds an order in the order system.',
version: '0.1.0',
input: z.object({ orderId: z.string() }),
output: z.object({ url: z.string() }),
effects: [],
mutating: false,
needsSpec: {
env: {
ORDERS_BASE_URL: { type: 'string', pattern: '^https://' },
ORDERS_REGION: { type: 'string', enum: ['eu', 'us'], default: 'eu' },
},
},
handler: async ({ orderId }, ctx) => {
const base = ctx.env?.ORDERS_BASE_URL;
if (base === undefined) throw new Error('ORDERS_BASE_URL is not set');
return { url: `${base}/${ctx.env?.ORDERS_REGION}/orders/${orderId}` };
},
});
if (defined.kind === 'err') {
throw new Error(`my-pack.find-order failed to compile: ${defined.error.message}`);
}
export default defined.value;
  • The declaration maps each name to a JSON Schema for its value. Values are strings, as in a process environment. The schema checks the string, with keywords such as enum, pattern or minLength. A name with a default is optional. Every other declared name is required. Each schema must compile as the runtime compiles it, strictly: an unknown keyword is an error. A default must be a string. A tool with a schema that doesn't compile is refused when it's defined, registered or deployed:

    Tool "my-pack.find-order": the schema for needsSpec.env.ORDERS_REGION doesn't compile: strict mode: unknown keyword: "enmu"
  • On every call, Kindgi resolves each declared name for the call: the project's value, else its org's, else the tenant's. It reads them in the env the runtime serves, which KINDGI_ENV names. Under kindgi dev, the env is local. A name no scope sets gets its default. A required name with no value, or a value that doesn't fit its schema, fails the call before the handler runs. The failure names the tool and the value, and gives the command that sets it.

  • The values a call used are recorded with it. A call re-run after a wait or a retry sees the same ones, even after someone changes them. An approval is one such wait. A comparison that replays the run sends a read-only tool it runs live the values the run's call saw, so a changed value can't skew it.

  • The context holds only what the tool declares. ctx.env is optional in the TypeScript type because a unit test builds its own context.

kindgi env set with --scope stores a value in the runtime, for a scope and an env. Set a value for the whole tenant, and override one for a single project:

Terminal window
pnpm exec kindgi env set ORDERS_BASE_URL https://orders.example.com --scope=tenant --env=local
pnpm exec kindgi env set ORDERS_REGION us --scope=project:<project-id> --env=local

Each command prints JSON with the value's env, scope, key and revision, and whether it replaced a value, then a line that confirms it. The first ends with:

…
✓ Set ORDERS_BASE_URL at tenant in env local. A tool that declares it (`needsSpec.env`) gets it on its next call.
Excerpt

The second prints the same line for ORDERS_REGION at project:<project-id>.

  • --scope is where the value lives: tenant, org:<orgId> or project:<projectId>.
  • <project-id> is the project's id. A call in that project now gets ORDERS_REGION=us. Every other call gets the default, eu.
  • --env is required with --scope. Name the env the runtime serves, which KINDGI_ENV names. Under kindgi dev, it's local. A value under another env name never reaches a call.
  • A new value counts on the next call. Nothing restarts, and no new agent version is needed.
  • Changing a value that's already set takes --force.
  • kindgi env list, with a --scope and an --env, shows that scope's values.
  • kindgi env unset NAME, with the same two flags, removes one. A call then gets the next scope's value again.

A call that needs a declared value no scope sets, and that has no default, fails before the tool runs. The message names the value and the command that sets it. For a tool acme-orders.needs-account that declares ACME_ACCOUNT_ID, which nobody set, it reads:

precondition-failed: Tool "acme-orders.needs-account" was not run: env-value-missing: tool "acme-orders.needs-account" needs env value "ACME_ACCOUNT_ID" in env "local", and none is set for project e889c1f5-eae7-45dc-8669-5bd029a5d85c, its org, or the tenant. Set it: kindgi env set ACME_ACCOUNT_ID <value> --scope=project:e889c1f5-eae7-45dc-8669-5bd029a5d85c --env=local (or --scope=tenant, for every project)

A test passes the values in the context it builds. Ask your coding agent: "Add a test for the order lookup tool." For reference, the test looks like this.

invokeToolForTest decides them as a run does:

  • a name the test doesn't set takes its schema's default;
  • each value is checked against its schema;
  • the handler sees the declared names only.

A missing or invalid value is precondition-failed, with the reason env-value-missing or env-value-invalid, and the handler doesn't run. Its third argument is the context. What it sets goes over the defaults of a test's context, which toolContextForTest builds:

tools/find-order/index.test.ts
import { invokeToolForTest } from '@kindgi/sdk/define';
import { expect, test } from 'vitest';
import findOrder from './index.js';
test('builds the URL from the env values', async () => {
const result = await invokeToolForTest(
findOrder,
{ orderId: 'ord_1001' },
{ env: { ORDERS_BASE_URL: 'https://orders.example.com' } },
);
// ORDERS_REGION isn't set, so it's its default, `eu`, as in a run.
expect(result).toEqual({
kind: 'ok',
value: { url: 'https://orders.example.com/eu/orders/ord_1001' },
});
});

Env values, settings, secrets, process env

Section titled “Env values, settings, secrets, process env”

A pack's code gets four kinds of values. Pick by what the value is:

The value… Use The code reads
should change only with a new agent version, judged before it goes live (a threshold, a prompt parameter) a settings block ctx.settings
differs per tenant, org or project, and isn't secret (a base URL, a region) an env value ctx.env
is a credential (an API key, a signing key) a secret ctx.secrets
is the pack service's own setup (its database, its SDK's config) the pack's environment process.env / os.environ

Env values aren't secret: they're recorded with each run that uses them and shown in its journal. kindgi env set warns about a name that looks like a credential, such as …_TOKEN, …_KEY or …_PASSWORD. Store those with kindgi secrets, as Give a tool a secret shows.