knight-ai-sdk
v0.2.0
Published
Lightweight TypeScript agent SDK — tool-using agents with run(), streaming, guardrails, and memory.
Maintainers
Readme
knight-ai-sdk
Lightweight TypeScript agent SDK for Node.js. Build tool-using agents with a goal-oriented run() loop, streaming, guardrails, and conversation memory — plus plain chat when you need it.
Not a heavy multi-agent framework: one client, one tool loop, batteries you can opt into.
Requires Node.js 18+.
Install
npm install knight-ai-sdk# At least one provider key
OPENAI_API_KEY=sk-...
# GROQ_API_KEY=gsk_...
# OPENROUTER_API_KEY=sk-or-...What you get
| Capability | Entry point | Use when |
|---|---|---|
| Agent orchestrator | ai.run() | Goal + tools + retries / stopWhen |
| Tool-using agent loop | ai.generateWithTools() | You own the message list; model calls tools until done |
| Token stream | ai.stream() | Backend → client SSE / WebSocket |
| One-shot completion | ai.generate() | Simple Q&A, no tools |
| Custom tools | defineTool from knight-ai-sdk/tools | App-specific agent actions |
| Built-in tools | createBuiltinTools | HTTP, file, calc, memory, … |
| Memory | WindowMemory / BufferMemory | Multi-turn agent context |
| Guardrails | guardrails on client or call | Cap rounds, tools, timeouts |
More detail: Getting started · Tool safety · Changelog
Quick start
import { KnightClient, WindowMemory } from "knight-ai-sdk";
import { createCalculatorTool, defineTool } from "knight-ai-sdk/tools";
const ai = new KnightClient({
// apiKeys: { openai: "...", groq: "...", openrouter: "..." },
guardrails: { maxRounds: 6, maxToolCalls: 10, toolTimeoutMs: 10_000 },
});
const shout = defineTool<{ text: string }>({
name: "shout",
description: "Uppercase text and add !!!",
parameters: {
type: "object",
properties: { text: { type: "string" } },
required: ["text"],
},
execute: ({ text }) => ({ shouted: `${text.toUpperCase()}!!!` }),
});
const result = await ai.run({
model: "openai/gpt-4o-mini",
goal: "Compute (2+3)*4, shout 'knight sdk', then summarize both.",
tools: [createCalculatorTool(), shout],
memory: new WindowMemory(20),
onEvent: (e) => {
if (e.type === "text_delta") process.stdout.write(e.text);
},
});
console.log(result.text);Client setup
import { KnightClient } from "knight-ai-sdk";
const ai = new KnightClient({
apiKeys: {
openai: process.env.OPENAI_API_KEY,
groq: process.env.GROQ_API_KEY,
openrouter: process.env.OPENROUTER_API_KEY,
},
// Defaults merged into every tool-loop call
guardrails: {
maxRounds: 8,
maxToolCalls: 20,
allowedTools: ["calculator", "http_request"],
deniedTools: ["shell_exec"],
toolTimeoutMs: 15_000,
},
});Keys resolve from apiKeys or env (OPENAI_API_KEY, GROQ_API_KEY, OPENROUTER_API_KEY). At least one key is required.
Imports
// Core client + memory + types
import {
KnightClient,
WindowMemory,
BufferMemory,
InMemoryStore,
createMemoryTools,
GuardrailError,
} from "knight-ai-sdk";
// Tools pack (optional)
import {
defineTool,
ToolRegistry,
createBuiltinTools,
createCalculatorTool,
// …
} from "knight-ai-sdk/tools";
// Python tool (separate entry — optional)
import { createPythonExecTool } from "knight-ai-sdk/tools/python";Providers and models
Model ids are provider/model (OpenRouter models may include an extra /):
| Provider | Example model id | Env key |
|---|---|---|
| OpenAI | openai/gpt-4o-mini | OPENAI_API_KEY |
| Groq | groq/llama-3.3-70b-versatile | GROQ_API_KEY |
| OpenRouter | openrouter/meta-llama/llama-3.3-70b-instruct | OPENROUTER_API_KEY |
await ai.generate({ model: "openai/gpt-4o-mini", messages: [...] });
await ai.generate({ model: "groq/llama-3.1-8b-instant", messages: [...] });
await ai.generate({
model: "openrouter/meta-llama/llama-3.3-70b-instruct",
messages: [...],
});Allowlists live in OPENAI_GPT_MODELS, GROQ_MODELS, and OPENROUTER_MODELS (exported from knight-ai-sdk). OpenRouter also accepts other org/model ids.
API guide
1. generate — one-shot completion
const result = await ai.generate({
model: "openai/gpt-4o-mini",
messages: [
{ role: "system", content: "Be brief." },
{ role: "user", content: "What is 2+2?" },
],
temperature: 0.2,
maxTokens: 256,
});
console.log(result.text);
console.log(result.finishReason);
// result.toolCalls — if you passed tools without running the loop yourselfWhen to use: single reply, no automatic tool execution.
2. stream — token / event stream
Returns an async generator. Useful for backends that forward chunks to a browser.
for await (const event of ai.stream({
model: "openai/gpt-4o-mini",
messages: [{ role: "user", content: "Count to five." }],
})) {
if (event.type === "text_delta") {
process.stdout.write(event.text); // or write SSE
}
if (event.type === "tool_call") {
console.log("tool:", event.toolCall.name, event.toolCall.arguments);
}
if (event.type === "done") {
console.log("\nfinish:", event.result.finishReason);
}
}| Event | Payload |
|---|---|
| text_delta | { text } — token chunk |
| tool_call | { toolCall } — completed call (name + args JSON string) |
| done | { result } — full GenerateResult |
Optional callback: onEvent on the same params (fires in addition to yield).
Backend → client (SSE sketch)
The SDK does not start an HTTP server; you bridge events:
import type { Request, Response } from "express";
app.post("/chat", async (req: Request, res: Response) => {
res.setHeader("Content-Type", "text/event-stream");
res.setHeader("Cache-Control", "no-cache");
res.flushHeaders?.();
for await (const event of ai.stream({
model: "openai/gpt-4o-mini",
messages: req.body.messages,
})) {
res.write(`data: ${JSON.stringify(event)}\n\n`);
}
res.end();
});For tool loops, use onEvent on run / generateWithTools and forward text_delta the same way.
3. generateWithTools — tool loop
Registers tools, calls the model, executes tool calls, appends results, repeats until the model stops or a guardrail trips.
const result = await ai.generateWithTools({
model: "openai/gpt-4o-mini",
messages: [{ role: "user", content: "What is (10+5)*2? Use the calculator." }],
tools: [createCalculatorTool()],
memory: new WindowMemory(20),
guardrails: { maxRounds: 4, maxToolCalls: 8 },
stream: true, // default — emits text_delta per round via onEvent
onEvent: (e) => {
if (e.type === "tool_start") console.log("calling", e.toolCall.name);
if (e.type === "text_delta") process.stdout.write(e.text);
},
});
console.log(result.text);
console.log(result.rounds, result.stoppedReason);
console.log(result.messages); // full transcript including tool messagesSet stream: false for non-streaming rounds.
4. run — agent orchestrator
Builds messages from goal (+ optional instructions / memory), then runs the tool-using agent loop. Adds optional retries and stopWhen.
const result = await ai.run({
model: "openai/gpt-4o-mini",
instructions: "Be concise. Prefer tools over guessing.",
goal: "Look up the weather for SF and summarize in one sentence.",
tools: [...],
memory: new WindowMemory(20),
guardrails: { maxRounds: 6, maxToolCalls: 12 },
maxRetries: 1,
stopWhen: (r) => r.text.length > 20,
stream: true, // default
onEvent: (e) => {
/* see Events below */
},
});| Option | Purpose |
|---|---|
| goal | User task (required) |
| instructions | Optional system prompt |
| maxRetries | Extra attempts after the first (default 0) |
| stopWhen | If returns false and retries remain, nudge and retry |
| stream | Stream each tool-loop round (default true) |
Prefer run for agents; use generateWithTools when you already own the message list.
Events (onEvent)
Tool-loop / run events (RunEvent):
| Type | When |
|---|---|
| round_start / round_end | Each model round |
| text_delta | Streaming tokens (if stream !== false) |
| tool_call | Model finished assembling a tool call |
| tool_start / tool_end | Before / after your execute |
| guardrail | Loop stopped by a limit / deny |
| retry | Orchestrator retry (run only) |
| final | Loop finished with GenerateWithToolsResult |
| done | Also appears from the underlying stream path |
Guardrails
Apply on the client (defaults) and/or per call:
guardrails: {
maxRounds: 6, // model rounds in the tool loop
maxToolCalls: 10, // total tool executions
allowedTools: ["calculator", "http_request"], // optional allowlist
deniedTools: ["shell_exec"], // optional denylist
toolTimeoutMs: 10_000, // per tool execution
}Violations emit { type: "guardrail", reason } and stop the loop (or throw GuardrailError where applicable).
Memory
Conversation memory
import { BufferMemory, WindowMemory } from "knight-ai-sdk";
const buffer = new BufferMemory(); // keep everything
const window = new WindowMemory(20); // last N messages
await ai.run({
goal: "Continue our earlier task…",
tools,
memory: window, // load before run, save after
});Implement ConversationMemory yourself for Redis/DB:
interface ConversationMemory {
load(): Message[] | Promise<Message[]>;
save(messages: Message[]): void | Promise<void>;
clear(): void | Promise<void>;
}Key/value memory tools
import { InMemoryStore, createMemoryTools } from "knight-ai-sdk";
// or from "knight-ai-sdk/tools"
const store = new InMemoryStore();
const { memoryGet, memorySet, memoryDelete, memoryList } = createMemoryTools(store);Vector tools (keyword / optional embeddings field)
import { createVectorStoreTools, InMemoryVectorStore } from "knight-ai-sdk/tools";
const vec = createVectorStoreTools(new InMemoryVectorStore());
// vec.vectorUpsert, vec.vectorQuery, vec.vectorDeleteDefault store is in-memory token overlap (not a hosted vector DB).
Custom tools
import { defineTool, ToolRegistry } from "knight-ai-sdk/tools";
const weather = defineTool<{ city: string }>({
name: "get_weather",
description: "Get current weather for a city",
parameters: {
type: "object",
properties: {
city: { type: "string", description: "City name" },
},
required: ["city"],
},
async execute({ city }) {
// your fetch / DB logic
return { city, tempC: 22, condition: "sunny" };
},
});
const registry = new ToolRegistry();
registry.register(weather);
await ai.generateWithTools({
model: "openai/gpt-4o-mini",
messages: [{ role: "user", content: "Weather in Paris?" }],
tools: registry.list(),
});execute runs in-process with full Node privileges — treat custom tools like server code. See tool safety.
Built-in tools pack
import { createBuiltinTools } from "knight-ai-sdk/tools";
const tools = createBuiltinTools({
// include: ["http", "file", "json", "calculator", "datetime", "uuid", "env", "memory", "vector"],
// file: { allowWrite: true }, // write opt-in
// include: [..., "shell"], // shell opt-in
// shell: { allowlist: ["node", "git"] },
// memory: myStore,
// vector: myVectorStore,
});| Group | Tool name(s) | Notes |
|---|---|---|
| http | http_request | http/https only, timeout |
| file | file_read, optional file_write | Sandbox root .knight-workspace; write off by default |
| json | json_util | parse / get / set / keys |
| calculator | calculator | + - * / ( ) only |
| datetime | datetime | now / format / add |
| uuid | uuid_generate | UUID v4 |
| env | env_get | Host env; blocks SECRET/TOKEN/API_KEY/… unless allowlisted |
| shell | shell_exec | Opt-in; allowlisted binaries; cwd sandboxed |
| memory | memory_* | Pluggable MemoryStore |
| vector | vector_* | Pluggable VectorStore |
| (separate) | python_exec | import { createPythonExecTool } from "knight-ai-sdk/tools/python" |
Individual factories (createHttpRequestTool, createCalculatorTool, …) are exported if you want a smaller set.
Production tip: start with calculator / datetime / uuid / memory; enable file/shell only with a sandbox and allowedTools. Details in tool-safety.md.
Choosing an API
Building an agent (goal + tools)? → run()
Tool loop with your own messages? → generateWithTools()
Need tokens on the wire? → stream() (or onEvent text_delta)
Need a single reply (no tools)? → generate()Local development (this repo)
npm install
cp .env.example .env # set provider keys for live examples only
npm run build
npm test # unit tests — no API credits
npm run example # live demo — uses provider credits
npm run pack:smoke # dry-run package contentssrc/
client/ KnightClient
core/ types, models, guardrails
providers/ OpenAI / Groq / OpenRouter (OpenAI-compatible)
memory/ conversation + KV store
tools/ registry + built-ins (+ python entry)
examples/
tests/
docs/