kenpachi
v0.3.17
Published
A production-shaped agent SDK: typed tools, time-travel context, self-healing tool calls, saga rollback, and dynamic tool synthesis.
Downloads
3,046
Maintainers
Readme
kenpachi

A small, typed agent SDK for building tool-using LLM agents in TypeScript —
built from scratch on top of raw provider fetch calls (no vendor SDK
dependency) with a few things most minimal agent loops skip:
📖 Full Documentation: https://kenpachi.mintlify.site/introduction
- Time-travel context — every turn is snapshotted; branch and resume from any prior point without re-calling the model for turns you already ran.
- Argument pre-coercion & validation — primitive arguments (like numeric or boolean strings) are automatically pre-coerced before Zod schema validation.
- Saga rollback — register a compensating action per tool call; if a later step in the same batch fails, already-completed steps are undone in reverse.
- KenpachiSDK dynamic tool synthesis — the model can author pure-logic tools
(math, parsing, formatting) at runtime. Anything needing a credential goes
through a
ConnectorRegistryyou configure ahead of time — the model writes the glue code, never the secret. - Pluggable memory —
InMemoryStorewith tokenized keyword matching & synonym search, plusMem0MemoryStoreadapter included. - Multi-agent handoffs — wrap specialist agents as tools with automatic message ordering safeguards.
Install
npm install kenpachiQuick start
import { z } from "zod";
import { Agent, defineTool, createAnthropicProvider } from "kenpachi";
const getWeather = defineTool({
name: "get_weather",
description: "Get the current weather for a city",
schema: z.object({ city: z.string() }),
async execute({ city }) {
return { city, tempC: 24, condition: "sunny" };
},
});
const provider = createAnthropicProvider({
apiKey: process.env.ANTHROPIC_API_KEY!,
model: "claude-sonnet-4-6",
});
const agent = new Agent(provider, [getWeather]);
const result = await agent.run("What's the weather in Nashik?");
console.log(result.text);Time-travel
await agent.run("first message");
const snap = agent.context.listSnapshots().at(-1)!;
// Branch back to that point and try a different follow-up, without
// re-running the first exchange against the model.
const branched = agent.context.branchAt(snap.turnIndex);Dynamic tools with a connector registry
import { ConnectorRegistry, synthesizeTool } from "kenpachi";
const registry = new ConnectorRegistry();
registry.register("weather", {
baseUrl: "https://api.openweathermap.org/data/2.5",
authEnvVar: "WEATHER_API_KEY", // secret lives in env, never in model output
description: "OpenWeatherMap current conditions",
});
const tool = synthesizeTool(
{
name: "get_weather",
description: "Fetches current weather for a city",
parameters: { city: "string" },
jsBody: `return await callConnector("/forecast?q=" + args.city);`,
connector: "weather",
},
registry
);Streaming
// Full event stream — token-by-token when the provider supports it
for await (const event of agent.stream("Tell me a story")) {
if (event.type === "text_delta") process.stdout.write(event.text);
if (event.type === "tool_call_start") console.log("\ncalling", event.name);
}
// Or the simpler shorthand on run():
const result = await agent.run("Tell me a story", {
onText: (chunk) => process.stdout.write(chunk),
});
console.log(result.text);agent.run() still works exactly as before — it's implemented as a thin wrapper
around stream() that returns the final AgentRunResult. Providers without
streamTurn() still work with both APIs; they just won't emit token-level deltas.
Handoffs
import { handoff } from "kenpachi";
const billingHandoff = handoff(billingAgent, "Use for billing or payment questions", {
id: "billing", // tool name becomes handoff_billing
});
const triageAgent = new Agent(provider, [billingHandoff, techSupportHandoff]);A handoff is a normal tool from the parent agent's point of view. Internally it
spawns the target agent with a fresh context (via Agent.spawn()), seeded with
the parent conversation according to context ("full" by default, or
"summary" / "none"), runs it to completion, and returns its answer as plain
text.
Security notes
- The sandbox (
src/sandbox.ts) uses Node'svmmodule for isolation, not a hard security boundary. It blocks accidental misuse (strayrequire, filesystem access) but is not a substitute for OS-level sandboxing (a separate worker process, gVisor, Firecracker) if you're running untrusted model output in a real production deployment. - Synthesized tools never receive raw secrets. Credentials are read from
process.envinside the connector layer and injected into outgoing requests server-side — the model only ever sees the connector name.
Development
npm install
npm run typecheck
npm run test
npm run buildLicense
MIT
