npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@kadi.build/core

v0.15.2

Published

A lean, readable SDK for building KADI agents.

Readme

kadi-core

TypeScript SDK for building KADI agents. Register tools, connect to brokers, load abilities, publish events, manage agent configuration, and run background processes.

kadi-core lets you:

  • Expose functions as callable tools over the network
  • Discover and invoke remote services without hardcoding URLs
  • Communicate via events across your service mesh
  • Load configuration from config.yml with project → global fallback and env var overrides
  • Manage agent.json configuration across projects, abilities, and global scope
  • Run background processes with headless, piped, and JSON-RPC bridge modes
  • Install as an ability — use kadi-core itself as a dependency in any agent

Install

npm install @kadi.build/core

Requirements:

  • Node.js 18+
  • TypeScript 5.0+ (recommended for full type inference)
  • ESM only ("type": "module" in your package.json)

Quick Start

Create a simple calculator agent:

// calculator.ts
import { KadiClient, z } from '@kadi.build/core';

const client = new KadiClient({
  name: 'calculator',
  version: '1.0.0',
});

client.registerTool({
  name: 'add',
  description: 'Add two numbers',
  input: z.object({ a: z.number(), b: z.number() }),
}, async ({ a, b }) => ({ result: a + b }));

// Start listening for requests over stdio
await client.serve('stdio');

Now another agent can load and use it:


Table of Contents


Core Concepts

| Concept | Plain English | |---------|---------------| | Tool | A function other services can call over the network (like an API endpoint) | | Agent | Your service. One codebase = one agent. Agents expose tools and call other agents' tools. | | Broker | The router/registry. Agents register with brokers so they can find each other (like DNS + API gateway combined) | | Ability | A tool provider you connect to. Could be in-process, a subprocess, or a remote service. | | Event | Fire-and-forget notification. Publish events, other agents can subscribe. |


Registering Tools

Tools are functions you expose for other agents to call.

import { KadiClient, z } from '@kadi.build/core';

const client = new KadiClient({ name: 'weather-service' });

// Define input schema with Zod
const weatherInput = z.object({
  city: z.string().describe('City name'),
  units: z.enum(['celsius', 'fahrenheit']).default('celsius'),
});

// Register tool
client.registerTool({
  name: 'getWeather',
  description: 'Get current weather for a city',
  input: weatherInput,
}, async ({ city, units }) => {
  const temp = await fetchTemperature(city);
  return { city, temperature: temp, units };
});

Why Zod? Type-safe schemas, 70% less code than JSON Schema, automatic TypeScript inference.

Targeting Specific Brokers

// Register tool only on specific brokers
client.registerTool(definition, handler, { brokers: ['internal'] });

Targeting Specific Networks

By default, a tool is visible on all networks the agent has joined. You can restrict a tool to specific networks using the networks option — the tool will only be discoverable and invocable by agents on those networks.

const client = new KadiClient({
  name: 'multi-network-agent',
  brokers: { default: 'ws://localhost:8080/kadi' },
  networks: ['global', 'internal', 'gpu-cluster'],
});

// Visible on ALL agent networks (global, internal, gpu-cluster)
client.registerTool({
  name: 'echo',
  description: 'Echo back the input',
  input: z.object({ message: z.string() }),
}, async ({ message }) => ({ echo: message }));

// Visible ONLY on gpu-cluster
client.registerTool({
  name: 'gpu-inference',
  description: 'Run ML inference on GPU',
  input: z.object({ model: z.string(), prompt: z.string() }),
}, async ({ model, prompt }) => {
  return { result: await runInference(model, prompt) };
}, { networks: ['gpu-cluster'] });

// Visible on internal AND global only (not gpu-cluster)
client.registerTool({
  name: 'audit-log',
  description: 'Retrieve audit logs',
  input: z.object({ limit: z.number() }),
}, async ({ limit }) => {
  return { logs: await getAuditLogs(limit) };
}, { networks: ['internal', 'global'] });

Per-tool networks must be a subset of the client's networks. Attempting to scope a tool to a network the agent hasn't joined throws an INVALID_CONFIG error:

// Throws KadiError — 'finance' is not in client networks
client.registerTool(definition, handler, { networks: ['finance'] });

You can combine brokers and networks to control both which brokers a tool registers with and which networks it's visible on:

client.registerTool(definition, handler, {
  brokers: ['production'],
  networks: ['internal'],
});

Connecting to Brokers

Brokers let agents discover and call each other without knowing URLs.

const client = new KadiClient({
  name: 'my-agent',
  brokers: {
    default: 'ws://localhost:8080/kadi',
    production: 'wss://broker.example.com/kadi',
  },
  defaultBroker: 'default',
  networks: ['global'],  // Which networks to join
});

// Connect (uses Ed25519 authentication)
await client.connect();

// Invoke a tool - broker routes to any available provider
const result = await client.invokeRemote('add', { a: 5, b: 3 });

// Disconnect when done
await client.disconnect();

Reconnection

Auto-reconnects with exponential backoff (1s → 2s → 4s → ... → 30s max) with jitter:

const client = new KadiClient({
  name: 'resilient-agent',
  brokers: { default: 'ws://localhost:8080/kadi' },
  autoReconnect: true,        // Default: true
  maxReconnectDelay: 30000,   // Cap at 30 seconds
});

Tool snapshot on reconnection: When an agent first registers with a broker, kadi-core captures a snapshot of the tools sent during that initial registration. On every subsequent reconnection, the same snapshot is replayed — tools that were registered locally after the initial connection (e.g. via loadNative) are not leaked to the broker.

This means abilities that use the native-mode pattern (connect first, then register tools locally) will correctly announce zero tools to the broker on reconnection, just as they did on the initial connect.

Event subscriptions on reconnection: All broker event subscriptions (registered via subscribe()) are automatically re-established on reconnect. Your event handlers continue working transparently — no manual resubscription needed.

If you dynamically register new tools after the initial connection and want the broker to know about them, call refreshBrokerTools():

// Register a new tool after the initial connection
client.registerTool({
  name: 'new-dynamic-tool',
  description: 'Added at runtime',
  input: z.object({ data: z.string() }),
}, async ({ data }) => ({ result: data }));

// Recapture the tool snapshot and re-announce to the broker
await client.refreshBrokerTools();

// Or target a specific broker
await client.refreshBrokerTools('production');

Lifecycle Hooks

Register callbacks for broker connection lifecycle events:

// Run cleanup logic when a broker disconnects
client.onDisconnect((brokerName) => {
  console.log(`Lost connection to ${brokerName}`);
});

// Run logic after a broker successfully reconnects
client.onReconnect((brokerName) => {
  console.log(`Reconnected to ${brokerName}`);
  // Good place to refresh state, re-sync data, etc.
});

Both hooks fire with the broker name and support async callbacks. Multiple hooks can be registered and will run in order. Errors in hooks are caught and logged — they won't break the reconnection flow.


Loading Abilities

Three ways to connect to other agents:

| Method | Use When | How It Works | |--------|----------|--------------| | loadNative(name) | Ability is a local TypeScript/JavaScript module | Runs in your process | | loadStdio(name) | Ability runs as a separate process (any language) | JSON-RPC over stdin/stdout | | loadBroker(name) | Ability is a remote service | WebSocket via broker |

loadNative — In-Process

// Resolves path from agent-lock.json
const calc = await client.loadNative('calculator');

// Or specify path directly
const calc = await client.loadNative('calculator', { path: './abilities/calc' });

// Use it
const result = await calc.invoke('add', { a: 5, b: 3 });
console.log(result); // { result: 8 }

// Cleanup (no-op for native, but good practice)
await calc.disconnect();

loadStdio — Child Process

By default, launches the ability using scripts.start from the ability's agent.json:

// Runs scripts.start from ability's agent.json
const processor = await client.loadStdio('image-processor');

// Use it
const result = await processor.invoke('resize', { width: 800 });
console.log(result);

// Cleanup (terminates child process)
await processor.disconnect();

You can specify a different script to launch:

// Run scripts.dev instead of scripts.start
const processor = await client.loadStdio('image-processor', { script: 'dev' });

Or bypass agent.json entirely with an explicit command:

// Explicit command (ignores agent.json)
const processor = await client.loadStdio('processor', {
  command: 'python3',
  args: ['main.py'],
});

loadBroker — Remote via Broker

// Must be connected to broker first
await client.connect();

// Find and connect to a remote ability
const gpu = await client.loadBroker('gpu-service', {
  networks: ['gpu-cluster'],
});

const result = await gpu.invoke('inference', { model: 'llama3' });
await gpu.disconnect();

Events from Abilities

Abilities can emit real-time events. Subscribe with on(), unsubscribe with off().

const watcher = await client.loadStdio('file-watcher');

// Define handlers (need references to unsubscribe later)
const onFileChanged = (data) => {
  console.log('File changed:', data.path, data.action);
};

const onError = (data) => {
  console.error('Watch error:', data.message);
};

// Subscribe to events
watcher.on('file.changed', onFileChanged);
watcher.on('file.error', onError);

// Later, unsubscribe
watcher.off('file.changed', onFileChanged);

// Cleanup (terminates child process)
await watcher.disconnect();

Emitting Events (from inside an ability)

If you're building an ability that emits events:

// Inside your ability code
client.emit('job.progress', { percent: 50, message: 'Halfway done' });
client.emit('file.changed', { path: '/tmp/foo.txt', action: 'modified' });

Broker Events (Pub/Sub)

When connected to a broker, agents can publish and subscribe to events across the network.

Subscribing

await client.connect();

// Subscribe to patterns (RabbitMQ topic exchange style)
client.subscribe('user.*', (event) => {
  console.log(`Channel: ${event.channel}`);   // 'user.login'
  console.log(`Data:`, event.data);           // { userId: '123' }
  console.log(`Network: ${event.networkId}`); // 'global'
  console.log(`Source: ${event.source}`);     // Publisher's session ID
});

Pattern matching:

  • user.* → matches user.login, user.logout (exactly one word after dot)
  • user.# → matches user, user.login, user.profile.update (zero or more words)
  • order.new → matches exactly order.new

Publishing

// Publish to default network
await client.publish('user.login', { userId: '123', timestamp: Date.now() });

// Publish to specific network
await client.publish('order.created', orderData, { network: 'internal' });

Unsubscribing

const handler = (event) => console.log(event);
client.subscribe('user.*', handler);

// Later, remove this specific handler
client.unsubscribe('user.*', handler);

Serving as an Ability

To make your agent callable by others:

Stdio Mode (for loadStdio)

const client = new KadiClient({ name: 'my-ability' });

client.registerTool({
  name: 'process',
  description: 'Process data',
  input: z.object({ data: z.string() }),
}, async ({ data }) => {
  return { processed: data.toUpperCase() };
});

// Serve over stdio - blocks until SIGTERM/SIGINT
await client.serve('stdio');

Broker Mode (for loadBroker)

const client = new KadiClient({
  name: 'my-service',
  brokers: { default: 'ws://localhost:8080/kadi' },
});

client.registerTool({ /* ... */ }, handler);

// Connect to broker and wait for requests
await client.serve('broker');

Agent.json Management

The AgentJsonManager provides unified read/write/delete access to agent.json files across three scopes:

| Scope | Location | Use Case | |-------|----------|----------| | Project | ./agent.json | Your agent's own configuration | | Ability | ./abilities/<name>@<version>/agent.json | Installed ability configuration | | Home | ~/.kadi/agent.json | Global CLI / user-level configuration |

Via KadiClient

The client exposes a lazy agentJson property — no extra instantiation needed:

const client = new KadiClient({ name: 'my-agent' });

// Read the full project config
const config = await client.agentJson.readProject();

// Read a nested field with dot-path notation
const target = await client.agentJson.readProject('deploy.local.target');

// Write a nested field (deep-merges into existing data)
await client.agentJson.writeProject('deploy.staging', {
  target: 'akash',
  replicas: 2,
});

// Delete a field
await client.agentJson.deleteProject('deploy.staging');

Standalone Usage

You can also use AgentJsonManager directly without a client:

import { AgentJsonManager } from '@kadi.build/core';

const ajm = new AgentJsonManager({
  projectRoot: '/path/to/my-agent',   // Optional, auto-detects via agent-lock.json
  kadiHome: '~/.kadi',                // Optional, defaults to ~/.kadi
  createOnWrite: true,                // Create file if missing on write (default: true)
});

const config = await ajm.readProject();

Reading Ability Config

// Read full config for an installed ability (resolves highest semver by default)
const calcConfig = await client.agentJson.readAbility('calculator');

// Read a specific field
const scripts = await client.agentJson.readAbility('calculator', {
  field: 'scripts.start',
});

// Pin to a specific version (useful when multiple versions are installed)
const old = await client.agentJson.readAbility('calculator', {
  version: '1.0.0',
});

Global Home Config

// Read global KADI configuration
const homeConfig = await client.agentJson.readHome();
const defaultBroker = await client.agentJson.readHome('brokers.default');

// Write to global config
await client.agentJson.writeHome('brokers.staging', 'wss://staging.example.com/kadi');

Discovery

// List all installed abilities (reads agent-lock.json)
const abilities = await client.agentJson.listAbilities();
// [{ name: 'calculator', version: '1.0.0', path: '/abs/path/abilities/[email protected]' }, ...]

// Check if an ability is installed
const has = await client.agentJson.hasAbility('calculator');

// Get all installed versions of an ability
const versions = await client.agentJson.getAbilityVersions('calculator');
// ['1.0.0', '2.0.0']

// Get resolved paths for all three scopes
const paths = await client.agentJson.getPaths();
// { project: '/abs/path/agent.json', home: '/home/user/.kadi/agent.json' }

Process Manager

The ProcessManager spawns and supervises background child processes in three modes:

| Mode | Communication | Use Case | |------|--------------|----------| | headless | None | Fire-and-forget tasks (docker build, git clone) | | piped | stdout/stderr streaming | Tasks where you need live output (deploys, logs) | | bridge | JSON-RPC 2.0 over stdio | Interactive workers (inference engines, language servers) |

Via KadiClient

const client = new KadiClient({ name: 'my-agent' });

// Headless — fire and forget
const build = await client.processes.spawn('build', {
  command: 'docker',
  args: ['build', '-t', 'myapp', '.'],
  mode: 'headless',
});

// Check status later
const status = client.processes.getStatus('build');
console.log(status); // 'running' | 'exited' | 'killed' | 'errored'

Piped Mode — Live Output

const deploy = await client.processes.spawn('deploy', {
  command: 'kadi',
  args: ['deploy', '--target', 'akash'],
  mode: 'piped',
  cwd: '/path/to/project',
  env: { KADI_ENV: 'production' },
});

// Stream stdout/stderr in real time
deploy.on('stdout', (chunk) => process.stdout.write(chunk));
deploy.on('stderr', (chunk) => process.stderr.write(chunk));
deploy.on('exit', ({ exitCode }) => console.log('Deploy exited:', exitCode));

// Get buffered output later
const output = deploy.getOutput();
console.log(output.stdout); // Full stdout string
console.log(output.stderr); // Full stderr string

Bridge Mode — JSON-RPC Workers

Bridge mode wraps the child process in the same Content-Length framed JSON-RPC protocol used by stdio transports. This lets you send requests to the child and receive responses:

const worker = await client.processes.spawn('inference', {
  command: 'python3',
  args: ['worker.py'],
  mode: 'bridge',
});

// Send a JSON-RPC request and await the response
const result = await worker.request('run-inference', {
  model: 'llama3',
  prompt: 'Explain kadi in one sentence',
});
console.log(result);

// Fire-and-forget notification (no response expected)
worker.notify('update-config', { temperature: 0.7 });

// Listen for notifications from the worker
worker.on('notification', ({ method, params }) => {
  console.log(`Worker says: ${method}`, params);
});

Lifecycle Management

// List all managed processes
const all = client.processes.list();
const running = client.processes.list({ mode: 'piped' }); // Filter by mode

// Get a handle to an existing process
const proc = client.processes.get('build');

// Get detailed info
const info = proc.getInfo();
// { id: 'build', pid: 12345, mode: 'piped', state: 'running', startedAt: ... }

// Kill a single process (SIGTERM → grace period → SIGKILL)
await proc.kill();

// Kill all managed processes
await client.processes.killAll();

// Wait for a process to exit
const exitInfo = await proc.waitForExit();
console.log(exitInfo.exitCode, exitInfo.signal);

// Clean up exited/errored entries
client.processes.prune();

// Graceful shutdown — kills all and cleans up
await client.processes.shutdown();

Timeout Auto-Kill

Processes can have an automatic timeout. When the timeout fires, the process is killed:

const task = await client.processes.spawn('migration', {
  command: 'node',
  args: ['migrate.js'],
  mode: 'piped',
  timeout: 60000, // Kill after 60 seconds if still running
});

Standalone Usage

import { ProcessManager } from '@kadi.build/core';

const pm = new ProcessManager();

const worker = await pm.spawn('worker', {
  command: 'python3',
  args: ['worker.py'],
  mode: 'bridge',
});

const result = await worker.request('echo', { message: 'hello' });
await pm.shutdown();

Encryption (Crypto)

client.crypto provides NaCl sealed-box encryption for secure inter-agent communication. Sealed boxes let anyone encrypt a message using the recipient's public key — only the recipient can decrypt it. The sender remains anonymous (use API tokens or signatures for authentication).

Keys are derived automatically from the agent's Ed25519 identity — no extra key management required.

Encrypting for Another Agent

// Agent A encrypts secrets for Agent B
const encrypted = agentA.crypto.encryptFor(
  JSON.stringify({ API_KEY: 'sk-123', DB_PASS: 'hunter2' }),
  agentB.publicKey  // Ed25519 identity key (auto-converted to X25519)
);

Decrypting

// Agent B decrypts
const plaintext = agentB.crypto.decrypt(encrypted);
const secrets = JSON.parse(plaintext);

Getting Your Encryption Public Key

// X25519 encryption key (base64) — share this with senders
const encryptionKey = client.crypto.publicKey;

// Ed25519 identity key — the standard client.publicKey
const identityKey = client.crypto.identityPublicKey;

Key Format Auto-Detection

encryptFor() accepts both key formats:

  • Ed25519 SPKI DER base64 (44 bytes decoded) — the standard client.publicKey format, auto-converted to X25519
  • Raw X25519 base64 (32 bytes decoded) — used directly, no conversion needed
// Both work:
client.crypto.encryptFor('secret', otherAgent.publicKey);        // Ed25519 → auto-converted
client.crypto.encryptFor('secret', otherAgent.crypto.publicKey); // X25519 → used directly

Installing as an Ability

kadi-core v0.10.0 ships its own agent.json with type: "ability", so it can be installed into any agent project using the standard kadi install workflow:

kadi install kadi-core

After installation, a postinstall script creates a symlink at node_modules/@kadi.build/core pointing to the installed ability directory. This means your code can use the standard import path regardless of where the ability lives on disk:

import { KadiClient, z } from '@kadi.build/core';

This is particularly useful for:

  • Python agents (via kadi-core-py) that need a reference to kadi-core's protocol definitions
  • Polyglot projects that install abilities via kadi install rather than npm install
  • Deployed agents where abilities are resolved from agent-lock.json

Configuration (loadConfig)

Load settings from config.yml with a standardized 3-tier resolution:

  1. Environment variablesPREFIX_KEY (highest priority)
  2. Project config.yml — walk up from CWD to find the nearest config.yml
  3. Global ~/.kadi/config.yml — shared KADI infrastructure settings
  4. Built-in defaults — hardcoded fallbacks (lowest priority)

Secrets (API keys, tokens) should never go in config.yml. Use kadi secret for credentials.

Basic Usage

import { loadConfig } from '@kadi.build/core';

const { config, configPath, source } = loadConfig({
  section: 'tunnel',
  envPrefix: 'KADI_TUNNEL',
  defaults: {
    server_addr: 'kadi.build:7835',
    tunnel_domain: 'tunnels.kadi.build',
  },
});

console.log(config.server_addr);  // from config.yml or default
console.log(configPath);           // '/path/to/config.yml' or null
console.log(source);               // 'project' | 'global' | 'default'

With a config.yml like:

tunnel:
  server_addr: kadi.build:7835
  tunnel_domain: tunnels.kadi.build

memory:
  database: kadi_memory
  embedding_model: text-embedding-3-small

Options

| Option | Type | Default | Description | |--------|------|---------|-------------| | section | string | (required) | Top-level YAML key to extract | | startDir | string | process.cwd() | Directory to start the upward walk | | filename | string | 'config.yml' | Config filename to search for | | envPrefix | string | — | Env var prefix for automatic overrides | | defaults | Record<string, unknown> | {} | Built-in defaults (lowest priority) |

Environment Variable Overrides

When envPrefix is set, loadConfig scans process.env for matching keys:

envPrefix: 'MEMORY'

MEMORY_DATABASE=my_db        →  config.database = 'my_db'
MEMORY_EMBEDDING_MODEL=ada   →  config.embedding_model = 'ada'

Values are coerced automatically: "true"true, "42"42, "3.14"3.14.

File Discovery Helpers

import { findConfigFile, findGlobalConfigFile } from '@kadi.build/core';

// Walk up from CWD to find config.yml
const projectConfig = findConfigFile();

// Walk up from a specific directory
const agentConfig = findConfigFile('config.yml', '/path/to/agent');

// Check ~/.kadi/config.yml
const globalConfig = findGlobalConfigFile();

Result Type

import type { ConfigResult, ConfigSource, LoadConfigOptions } from '@kadi.build/core';

interface ConfigResult<T = Record<string, unknown>> {
  config: T;              // Merged config object
  configPath: string | null;  // Path to config.yml used, or null
  source: ConfigSource;   // 'project' | 'global' | 'default'
}

Note: loadConfig requires js-yaml at runtime. It is lazy-imported — if your project doesn't use loadConfig, you don't need js-yaml installed.


Error Handling

All errors are KadiError with structured codes and context:

import { KadiError } from '@kadi.build/core';

try {
  await client.invokeRemote('unknown-tool', {});
} catch (error) {
  if (KadiError.isKadiError(error)) {
    console.log(error.code);      // 'TOOL_NOT_FOUND'
    console.log(error.message);   // Human-readable message
    console.log(error.context);   // { toolName: '...', hint: '...' }
  }
}

Error Codes

| Code | When It Happens | |------|-----------------| | TOOL_NOT_FOUND | invoke() or invokeRemote() with unknown tool name | | BROKER_NOT_CONNECTED | Any broker operation before calling connect() | | ABILITY_NOT_FOUND | loadNative/loadStdio when ability not in agent-lock.json | | ABILITY_LOAD_FAILED | Ability exists but failed to start or initialize | | TOOL_INVOCATION_FAILED | Tool threw an error during execution | | BROKER_TIMEOUT | Request to broker timed out | | INVALID_CONFIG | Configuration error (missing required fields, etc.) | | LOCKFILE_NOT_FOUND | loadNative/loadStdio when agent-lock.json doesn't exist | | AGENT_JSON_NOT_FOUND | readProject/readAbility/readHome when agent.json doesn't exist | | AGENT_JSON_PARSE_ERROR | agent.json exists but contains invalid JSON | | AGENT_JSON_WRITE_ERROR | Failed to write agent.json (permissions, disk, etc.) | | AGENT_JSON_FIELD_NOT_FOUND | Dot-path field doesn't exist in agent.json | | AGENT_JSON_VERSION_AMBIGUOUS | Multiple versions installed and no version specified | | PROCESS_ALREADY_EXISTS | spawn() with an ID that's already in use | | PROCESS_NOT_FOUND | get() / getStatus() with an unknown process ID | | PROCESS_NOT_RUNNING | request() / notify() on an exited process | | PROCESS_BRIDGE_ERROR | JSON-RPC communication error in bridge mode | | PROCESS_SPAWN_FAILED | Child process failed to start (bad command, permissions) | | PROCESS_TIMEOUT | Process exceeded its configured timeout |


API Reference

KadiClient

new KadiClient(config: ClientConfig)

Configuration:

| Option | Type | Default | Description | |--------|------|---------|-------------| | name | string | required | Agent name | | version | string | '1.0.0' | Agent version | | brokers | Record<string, string> | {} | Broker name → URL map | | defaultBroker | string | first broker | Default broker to use | | networks | string[] | ['global'] | Networks to join | | autoReconnect | boolean | true | Auto-reconnect on disconnect | | maxReconnectDelay | number | 30000 | Max backoff delay (ms) | | requestTimeout | number | 30000 | Default request timeout (ms) |

Methods:

| Method | Description | |--------|-------------| | registerTool(def, handler, options?) | Register a tool. See Registering Tools | | connect(broker?) | Connect to broker | | disconnect(broker?) | Disconnect from broker | | invokeRemote(tool, params, options?) | Invoke tool via broker (broker picks provider) | | loadNative(name, options?) | Load ability in-process. See Loading Abilities | | loadStdio(name, options?) | Load ability as child process | | loadBroker(name, options?) | Load ability via broker | | subscribe(pattern, handler, options?) | Subscribe to broker events. See Broker Events | | unsubscribe(pattern, handler, options?) | Unsubscribe from events | | publish(channel, data, options?) | Publish event to broker | | emit(event, data) | Emit event to consumer (when serving as ability) | | serve(mode) | Serve as ability ('stdio' or 'broker') | | refreshBrokerTools(broker?) | Recapture tool snapshot and re-announce to broker. See Reconnection | | onDisconnect(hook) | Register callback for broker disconnect. See Lifecycle Hooks | | onReconnect(hook) | Register callback for broker reconnect. See Lifecycle Hooks | | getConnectedBrokers() | List connected broker names |

Properties:

| Property | Type | Description | |----------|------|-------------| | agentJson | AgentJsonManager | Lazy-initialized agent.json manager. See Agent.json Management | | processes | ProcessManager | Lazy-initialized process manager. See Process Manager | | crypto | CryptoService | Lazy-initialized encryption service. See Encryption (Crypto) |

LoadedAbility

Returned by loadNative(), loadStdio(), loadBroker():

| Property/Method | Description | |-----------------|-------------| | name | Ability name | | transport | 'native' | 'stdio' | 'broker' | | invoke(tool, params) | Invoke a tool | | getTools() | Get array of tool definitions | | on(event, handler) | Subscribe to events | | off(event, handler) | Unsubscribe from events | | disconnect() | Cleanup resources |

BrokerEvent

Received when subscribed to broker events:

interface BrokerEvent {
  channel: string;      // 'user.login'
  data: unknown;        // Event payload
  networkId: string;    // Which network
  source: string;       // Publisher's session ID
  timestamp: number;    // Unix timestamp (ms)
}

AgentJsonManager

new AgentJsonManager(options?: AgentJsonManagerOptions)

Options:

| Option | Type | Default | Description | |--------|------|---------|-------------| | projectRoot | string | auto-detect | Path to agent project root | | kadiHome | string | ~/.kadi | Path to KADI home directory | | createOnWrite | boolean | true | Create agent.json if missing on write |

Methods:

| Method | Returns | Description | |--------|---------|-------------| | readProject(field?) | Promise<unknown> | Read project agent.json (optional dot-path field) | | readAbility(name, options?) | Promise<unknown> | Read an installed ability's agent.json | | readHome(field?) | Promise<unknown> | Read global ~/.kadi/agent.json | | writeProject(path, value) | Promise<void> | Write/deep-merge a field in project agent.json | | writeAbility(name, path, value, version?) | Promise<void> | Write a field in an ability's agent.json | | writeHome(path, value) | Promise<void> | Write a field in global agent.json | | deleteProject(path) | Promise<boolean> | Delete a field from project agent.json | | deleteAbility(name, path, version?) | Promise<boolean> | Delete a field from an ability's agent.json | | deleteHome(path) | Promise<boolean> | Delete a field from global agent.json | | listAbilities() | Promise<AbilityInfo[]> | List all installed abilities | | hasAbility(name) | Promise<boolean> | Check if an ability is installed | | getAbilityVersions(name) | Promise<string[]> | Get installed versions of an ability | | getPaths() | Promise<AgentJsonPaths> | Get resolved file paths for all scopes |

ProcessManager

new ProcessManager()

Methods:

| Method | Returns | Description | |--------|---------|-------------| | spawn(id, options) | Promise<ManagedProcess> | Spawn a new background process | | get(id) | ManagedProcess | Get handle to an existing process (throws if not found) | | getStatus(id) | ProcessState | Get process state: 'running' | 'exited' | 'killed' | 'errored' | | list(options?) | ManagedProcess[] | List all managed processes (optional mode filter) | | kill(id) | Promise<void> | Kill a process by ID | | killAll() | Promise<void> | Kill all managed processes | | shutdown() | Promise<void> | Kill all and clean up | | prune() | number | Remove exited/errored entries, returns count pruned |

SpawnOptions:

| Option | Type | Default | Description | |--------|------|---------|-------------| | command | string | required | Command to run | | args | string[] | [] | Command arguments | | mode | ProcessMode | required | 'headless' | 'piped' | 'bridge' | | cwd | string | process.cwd() | Working directory | | env | Record<string, string> | inherited | Environment variables | | timeout | number | none | Auto-kill after N milliseconds | | killGracePeriod | number | 5000 | Time between SIGTERM and SIGKILL |

ManagedProcess

Returned by ProcessManager.spawn(). Extends EventEmitter.

| Property/Method | Description | |-----------------|-------------| | id | Unique process identifier | | pid | OS process ID | | mode | 'headless' | 'piped' | 'bridge' | | state | Current state: 'running' | 'exited' | 'killed' | 'errored' | | request(method, params?) | Bridge only. Send JSON-RPC request, returns Promise<unknown> | | notify(method, params?) | Bridge only. Send JSON-RPC notification (no response) | | write(data) | Write raw data to stdin | | kill() | Kill the process (SIGTERM → grace → SIGKILL) | | waitForExit() | Promise<ProcessExitInfo> — resolves when process exits | | getOutput() | Piped only. Get buffered { stdout, stderr } | | getInfo() | Get ProcessInfo snapshot |

Events:

| Event | Payload | Modes | |-------|---------|-------| | stdout | string | piped | | stderr | string | piped | | exit | ProcessExitInfo | all | | error | Error | all | | notification | { method, params } | bridge |

CryptoService

Accessed via client.crypto. Lazy-initialized from the agent's Ed25519 identity keys.

Properties:

| Property | Type | Description | |----------|------|-------------| | publicKey | string | X25519 encryption public key (base64). Share with senders. Cached after first access | | identityPublicKey | string | Ed25519 identity public key (SPKI DER base64). Same as client.publicKey |

Methods:

| Method | Returns | Description | |--------|---------|-------------| | encryptFor(plaintext, recipientPublicKey) | string | Encrypt string for recipient. Returns base64 sealed box. Accepts Ed25519 or X25519 keys | | decrypt(ciphertext) | string | Decrypt base64 sealed box. Throws on failure |


Troubleshooting

"Cannot find module" with loadNative/loadStdio

The ability isn't in your agent-lock.json. Either:

  • Run kadi install to install abilities and generate the lock file
  • Use the path option: loadNative('calc', { path: './path/to/ability' })
  • Use explicit command: loadStdio('calc', { command: 'node', args: ['calc.js'] })

Connection timeout to broker

  1. Check the broker is running
  2. Verify the URL uses ws:// or wss://, not http://
  3. Check firewall/network allows WebSocket connections

Events not being received

  1. Verify both publisher and subscriber are on the same network (check networks config)
  2. Check your pattern matches the channel (remember: * = one word, # = zero or more)
  3. Ensure you're connected before subscribing: await client.connect()

Tool invocation times out

Increase the timeout:

await client.invokeRemote('slow-tool', params, { timeout: 60000 });

AGENT_JSON_NOT_FOUND errors

The manager can't find agent.json at the expected path. Either:

  • Ensure you're running from inside a project with agent.json and agent-lock.json
  • Pass an explicit projectRoot when constructing AgentJsonManager

PROCESS_BRIDGE_ERROR in bridge mode

The child process isn't speaking the expected Content-Length framed JSON-RPC protocol. Ensure:

  1. The child writes Content-Length: <n>\r\n\r\n<json> to stdout
  2. The child reads the same framing from stdin
  3. No other output is mixed into stdout (use stderr for logging)

Advanced: Building CLI Tools

These utilities are exported for building tooling on top of kadi-core.

Zod to JSON Schema

Convert Zod schemas to JSON Schema (useful for generating documentation or OpenAPI specs):

import { zodToJsonSchema, z } from '@kadi.build/core';

const schema = z.object({ name: z.string(), age: z.number() });
const jsonSchema = zodToJsonSchema(schema);
// { type: 'object', properties: { name: { type: 'string' }, ... } }

Lock File Resolution

Utilities for working with agent-lock.json (the file that tracks installed abilities):

import {
  findProjectRoot,
  readLockFile,
  resolveAbilityPath,
  getInstalledAbilityNames,
} from '@kadi.build/core';

// Find the nearest directory containing agent-lock.json
const root = findProjectRoot();

// Read and parse the lock file
const lock = readLockFile(root);

// List installed ability names
const abilities = getInstalledAbilityNames(lock);
// ['calculator', 'image-processor', ...]

// Get the filesystem path to an ability
const calcPath = resolveAbilityPath('calculator', root);
// '/path/to/project/abilities/[email protected]'

Dot-Path Utilities

Low-level helpers for nested object access (used internally by AgentJsonManager):

import { getByPath, setByPath, deleteByPath, deepMerge } from '@kadi.build/core';

const obj = { deploy: { local: { target: 'docker' } } };

getByPath(obj, 'deploy.local.target');  // 'docker'
setByPath(obj, 'deploy.staging.target', 'akash');
deleteByPath(obj, 'deploy.local');

// Deep merge (objects merged recursively, arrays replaced)
const merged = deepMerge(
  { deploy: { replicas: 1, target: 'docker' } },
  { deploy: { replicas: 3 } },
);
// { deploy: { replicas: 3, target: 'docker' } }

Stdio Framing

For building custom Content-Length framed JSON-RPC protocols (used internally by stdio transport and bridge mode):

import { StdioMessageReader, StdioMessageWriter } from '@kadi.build/core';

// Wrap a readable stream (e.g., child.stdout)
const reader = new StdioMessageReader(childProcess.stdout);
reader.onMessage((msg) => console.log('Received:', msg));

// Wrap a writable stream (e.g., child.stdin)
const writer = new StdioMessageWriter(childProcess.stdin);
writer.write({ jsonrpc: '2.0', method: 'ping', params: {} });

Related


License

MIT