Skip to content

template-agent-typescript

TypeScript agent template

template-agent-typescript is a starter KĀDI agent that connects to a broker, registers local tools, optionally loads provider credentials and native abilities, and can start optional Slack/Discord bot adapters. It exists to provide a minimal, well-structured TypeScript example showing how to build agents that expose tools to the KĀDI orchestrator, bridge native abilities, and integrate provider credentials and persistent memory.

  • BaseAgent lifecycle: The agent builds a BaseAgentConfig from config.toml (and env vars / vault credentials), constructs a BaseAgent, registers local tools on the agent’s KadiClient, then connects to the broker. After connection, it can start optional bot adapters (Slack/Discord). See src/index.ts for the main flow.
  • Tool registration: Tools are registered locally via the tool registry (src/tools/index.ts → registerAllTools). Each register function calls client.registerTool(…) so the orchestrator and other agents can discover and invoke the tools.
  • Native ability bridging: Abilities loaded natively (loadNative()) keep their tools isolated. src/tools/native-ability-bridge.ts provides registerNativeAbilityTools(client, ability, prefix?) which registers wrappers on the host client that delegate to ability.invoke(…), and patches the stored input schema with the ability’s original JSON Schema so LLMs see accurate parameter schemas.
  • Provider & secrets flow: The agent loads vault credentials via loadVaultCredentials(). Environment variables override vault values. The agent then constructs provider config (anthropic, model-manager) into BaseAgentConfig so the providerManager in BaseAgent can initialize chosen providers.
  • Memory & bots: BaseAgent exposes memoryService and providerManager instances. When enabled, bots (Slack/Discord) are instantiated with client, provider manager, and memory service so they can interact with tools and providers through the KadiClient.

How it fits in the AGENTS ecosystem:

  • This agent is a Kadi client that announces its tools and responds to tool invocations.
  • Other agents or orchestrator LLMs discover the agent’s tools via client.readAgentJson().tools; they can call local tools (invokeTool) or call network tools via invokeRemote semantics provided by the KADI runtime.
  • Abilities (native modules) can be loaded and bridged so their tools are visible in the orchestrator.

The package exports tool registration functions and a registry helper. The following table documents the primary exports and registered tools.

Export / ToolFunction / ObjectDescriptionKey parameters / notes
registerEchoToolfunction registerEchoTool(client: KadiClient): voidRegisters the “echo” tool which returns the input text and its length.Input: { text: string } (echoInputSchema). Output: { echo: string, length: number } (echoOutputSchema).
registerListToolsToolfunction registerListToolsTool(client: KadiClient): voidRegisters “list_tools” utility to return a human-readable list of local + network tools in both structured and presentation forms.No input params. Output includes status, result.tools (name + description), counts, and presentation hints for LLMs.
toolRegistryArray<(client: KadiClient) => void>Array of tool registration functions. Add custom register functions here.items: registerEchoTool, registerListToolsTool (see src/tools/index.ts)
registerAllToolsfunction registerAllTools(client: KadiClient): voidCalls every function in toolRegistry to register tools on the KadiClient.Logs timing and counts.
registerNativeAbilityToolsfunction registerNativeAbilityTools(client: KadiClient, ability: LoadedAbility, prefix?: string): numberBridge that registers all tools from a natively-loaded ability onto the host client. Delegates invocations to ability.invoke().Returns number of tools registered. Registers with z.any() then patches registered.definition.inputSchema with original JSON Schema from the ability.

If you plan to expose additional tools:

  • Add a file exporting a registerXTool(client) function and add that function to toolRegistry in src/tools/index.ts.

Configuration is read from config.toml (via readConfig()) with environment variable overrides and credential vault fallback via loadVaultCredentials(). Key config fields and environment variables used by this agent:

config.toml fields

  • [agent]
    • ID — agent identifier (string) (used for setAgentTag)
    • VERSION — agent version
    • ROLE — agent role (e.g., “programmer”)
  • [logging]
    • LEVEL — log level (e.g., “debug”, “info”)
  • [broker.local]
    • URL — local broker WebSocket URL (e.g., ws://localhost:8080/kadi)
    • NETWORKS — array of network names to join (e.g., [“chatbot”])
  • [broker.remote] (optional)
    • URL — remote broker WebSocket URL
    • NETWORKS — array of networks for remote broker
  • [provider]
    • PRIMARY — primary provider key (e.g., “model-manager”)
    • FALLBACK — fallback provider key (e.g., “anthropic”)
  • [provider.]
    • MODEL — model identifier for that provider (e.g., provider.anthropic.MODEL)
  • [memory]
    • DATA_PATH — local path for memory persistence
  • [secrets]
    • VAULTS — array of named vaults to load (e.g., [“anthropic”,“model-manager”])
  • [bot.slack] and [bot.discord]
    • ENABLED — “true” to start the adapter
    • USER_ID — bot user id to filter events

Environment variables (take precedence over config/vault where applicable)

  • KADI_BROKER_URL_LOCAL — overrides broker.local.URL
  • KADI_BROKER_URL_REMOTE — overrides broker.remote.URL
  • MEMORY_DATA_PATH — overrides memory.DATA_PATH
  • ANTHROPIC_API_KEY — direct Anthropics API key override
  • MODEL_MANAGER_BASE_URL — model-manager base URL override
  • MODEL_MANAGER_API_KEY — model-manager API key override

Secrets / Vault

  • The agent calls loadVaultCredentials() which loads secrets from configured vaults (config.toml [secrets].VAULTS). The loaded vault object is referenced in src/index.ts as vault and used to populate:
    • vault.ANTHROPIC_API_KEY
    • vault.MODEL_MANAGER_BASE_URL
    • vault.MODEL_MANAGER_API_KEY
  • The agent.json declares an ability “secret-ability” which indicates a runtime dependency for secret management; the agent relies on vault secrets but env vars take priority.

Notes:

  • The agent requires at least one broker configuration: either broker.local.* or broker.remote.*. The code throws an error if neither is present.
  • If both local and remote brokers are defined, the agent supports additionalBrokers in the BaseAgentConfig.

Below are representative, copy-pasted code snippets from the package to show key patterns.

  • Registering the echo tool (src/tools/echo.ts):
export const echoInputSchema = z.object({
text: z.string().describe('Text to echo back')
});
export const echoOutputSchema = z.object({
echo: z.string().describe('Echoed text'),
length: z.number().describe('Length of text')
});
export type EchoInput = z.infer<typeof echoInputSchema>;
export type EchoOutput = z.infer<typeof echoOutputSchema>;
export function registerEchoTool(client: KadiClient): void {
client.registerTool({
name: 'echo',
description: 'Echo back the input text with its length (placeholder tool - replace with your own)',
input: echoInputSchema,
output: echoOutputSchema
}, async (params: EchoInput): Promise<EchoOutput> => {
logger.info(MODULE_AGENT, `Echoing text: "${params.text}"`, timer.elapsed('main'));
const result = {
echo: params.text,
  • List tools utility (src/tools/list-tools.ts) — registration + schemas:
export const listToolsInputSchema = z.object({});
export const listToolsOutputSchema = z.object({
status: z.enum(['info', 'complete', 'partial', 'error']).describe('Result status: info for queries, complete for task-completing actions'),
result: z.object({
tools: z.array(z.object({
name: z.string().describe('Tool name'),
description: z.string().describe('Tool description')
})).describe('Array of all available tools'),
count: z.object({
local: z.number().describe('Number of local tools'),
network: z.number().describe('Number of network tools'),
total: z.number().describe('Total number of tools')
}).describe('Tool counts')
}).describe('Structured tool listing data'),
presentation: z.object({
summary: z.string().describe('Short summary for quick reference'),
details: z.string().describe('Full formatted tool list with descriptions'),
format_hint: z.string().describe('Guidance for LLM on how to present this data to the user')
}).describe('Presentation layer for LLM to customize output')
});
export type ListToolsOutput = z.infer<typeof listToolsOutputSchema>;
export function registerListToolsTool(client: KadiClient): void {
client.registerTool({
name: 'list_tools',
description: 'List all available tools in human-readable format. This is an informational query — it does NOT complete the user\'s task. After listing tools, continue to fulfill the user\'s actual request using the appropriate tool.',
input: listToolsInputSchema,
output: listToolsOutputSchema
}, async (): Promise<ListToolsOutput> => {
logger.info(MODULE_AGENT, 'Listing all available tools', timer.elapsed('main'));
try {
// 1. Get local tools (registered on this agent)
const localTools = client.readAgentJson().tools;
  • Native ability bridge (src/tools/native-ability-bridge.ts) — full bridge function:
export function registerNativeAbilityTools(
client: KadiClient,
ability: LoadedAbility,
prefix?: string,
): number {
const tools = ability.getTools();
for (const tool of tools) {
const toolName = prefix ? `${prefix}_${tool.name}` : tool.name;
// Register with z.any() — handler delegates to ability.invoke()
client.registerTool(
{
name: toolName,
description: tool.description || tool.name,
input: z.any(),
},
async (params: unknown) => {
return ability.invoke(tool.name, params as Record<string, unknown>);
},
);
// Patch inputSchema with the ability's original JSON Schema
// so the LLM knows what parameters to pass
const registered = (client as any).tools.get(toolName);
if (registered && tool.inputSchema) {
registered.definition.inputSchema = tool.inputSchema;
}
}
return tools.length;
}
  • Main startup flow (src/index.ts) — config parsing, building BaseAgentConfig, registering tools, connecting:
const cfg = readConfig();
const agentId = cfg.string('agent.ID');
const agentVersion = cfg.string('agent.VERSION');
const logLevel = cfg.has('logging.LEVEL') ? cfg.string('logging.LEVEL') : 'info';
setLogLevel(logLevel);
setAgentTag(agentId);
const hasLocal = cfg.has('broker.local.URL');
const hasRemote = cfg.has('broker.remote.URL');
if (!hasLocal && !hasRemote) {
throw new Error('At least one broker required: set [broker.local] or [broker.remote] in config.toml');
}
const brokerUrl = hasLocal
? (process.env.KADI_BROKER_URL_LOCAL ?? cfg.string('broker.local.URL'))
: (process.env.KADI_BROKER_URL_REMOTE ?? cfg.string('broker.remote.URL'));
const networks = hasLocal
? cfg.strings('broker.local.NETWORKS')
: cfg.strings('broker.remote.NETWORKS');
const baseAgentConfig: BaseAgentConfig = {
agentId,
agentRole: cfg.has('agent.ROLE') ? cfg.string('agent.ROLE') : 'programmer',
version: agentVersion,
brokerUrl,
networks,
...(additionalBrokerUrl ? {
additionalBrokers: {
remote: { url: additionalBrokerUrl, networks: additionalBrokerNetworks! },
},
} : {}),
...((anthropicApiKey || (modelManagerBaseUrl && modelManagerApiKey)) ? {
provider: {
...(anthropicApiKey ? { anthropicApiKey } : {}),
...(modelManagerBaseUrl && modelManagerApiKey ? {
modelManagerBaseUrl,
modelManagerApiKey,
} : {}),
...(primaryProvider ? { primaryProvider } : {}),
...(fallbackProvider ? { fallbackProvider } : {}),
},
} : {}),
memory: {
dataPath: process.env.MEMORY_DATA_PATH ?? cfg.string('memory.DATA_PATH'),
},
};
const baseAgent = new BaseAgent(baseAgentConfig);
const client = baseAgent.client;
// Register local tools before connecting
registerAllTools(client);
logger.info(agentId, `Tools: ${client.readAgentJson().tools.length} registered`, timer.elapsed('main'));
// Connect to broker
await baseAgent.connect();

Declared in package metadata and used at runtime:

Runtime dependencies (from package manifest)

  • @anthropic-ai/sdk — optional provider integration
  • @kadi.build/core — KADI core types and client primitives (local file: link in the source)
  • agents-library — BaseAgent, provider/memory helpers, logging, timer utilities (local file: link)
  • zod — schema handling for tools (exposed via @kadi.build/core/zod usage in tools files)

Dev dependencies

  • typescript, tsx, eslint, vitest, etc. (standard TypeScript tooling)

Abilities (from agent.json)

  • secret-ability: ^0.9.4 — declared ability used to manage secrets / vault integration at runtime.

What depends on this package

  • Consumers/orchestrators: Other agents and the LLM orchestrator will discover and invoke its tools via the broker and KadiClient tool registry. Bot adapters (Slack/Discord) included in this package depend on the agent’s client/provider/memory but are internal to this package.

What this package depends on (runtime):

  • Kadi runtime and agents-library for BaseAgent, provider and memory features.
  • The ability runtime (native abilities) if loadNative() is used; the native-ability-bridge will bridge tools from LoadedAbility into the KadiClient registry.
  • To add a tool: create a new file under src/tools that exports a registerXTool(client: KadiClient) function, then add it to toolRegistry in src/tools/index.ts.
  • Use zod schemas for input and output where possible — these are exposed to the orchestrator/LLM. When bridging native abilities, the bridge will patch JSON Schema onto the tool registration so LLMs get accurate schemas.
  • Credential precedence: process.env -> vault -> absent. The code expects ANTHROPIC_API_KEY and model-manager fields to come from either env or vault.
  • Bots are optional and only started when config flags are enabled (“true”). They rely on providerManager and memoryService instances from BaseAgent.

If you need to modify startup, look at src/index.ts; to add tools, modify src/tools/index.ts and add a new register function. For bridging third-party abilities into the orchestrator-visible tool list, use registerNativeAbilityTools as a reference/utility.