Shirube
Shirube is a TypeScript SDK for building production AI agents. You define an agent (instructions, model, tools), call run(), and Shirube owns the loop: talk to the LLM, execute tools, hand off to specialists, enforce guardrails, remember the user, and stop safely.
It is named after its author. The core loop is written in this repo — it is not a wrapper around the OpenAI or Claude agent frameworks.
import { Agent, tool } from "shirube-ai";
import { z } from "zod";
const weather = tool({
name: "get_weather",
description: "Current weather for a city.",
parameters: z.object({ city: z.string() }),
execute: async ({ input }) => ({ city: input.city, tempC: 21 }),
});
const agent = Agent.builder()
.name("assistant")
.instructions("Be concise. Use tools when you need facts.")
.apiKey(process.env.OPENAI_API_KEY!)
.tools([weather])
.build();
const result = await agent.run("Weather in Paris?");
console.log(result.output); // final answer
console.log(result.model, result.turns, result.runId);
Who it is for
Use Shirube when you are shipping an agent that other people will actually talk to:
| Use case | What Shirube gives you |
|---|---|
| Customer support | Tools for tickets/refunds, sessions per chat, graph memory of the customer, handoff to billing |
| Internal ops | MCP tools from GitHub/Linear, approval on risky actions, traces for audit |
| Coding / research assistants | Model routing (cheap model for easy questions), streaming UI events, structured JSON output |
| Multi-agent products | One triage agent that transfers to specialists without looping |
If you only need a one-off Chat Completions call, use the vendor SDK. Shirube exists for the rest of the stack: tools, memory, security, graph knowledge, MCP, and observability.
Why it exists
Vendor agent kits give you a loop. Production still needs:
- Which model to call for this prompt (and a fallback if that provider is down)
- How to remember a user across weeks without stuffing the whole history into every request
- How to stop jailbreaks, leaks, and unapproved refunds
- How to share tools with Cursor via MCP
- How to keep a knowledge graph from filling with duplicates
Shirube implements those in-process, with security on by default.
Install
npm install shirube-ai zod
Requires Node 18.18+. Set OPENAI_API_KEY, or pass .apiKey(). Optional: mem0ai for hosted long-term memory, ANTHROPIC_API_KEY / GEMINI_API_KEY for other providers.
How an agent run works
- Input guardrails — reject jailbreaks, redact PII, enforce size limits.
- Load context — session history, long-term memory hits, graph facts for this user.
- Pick a model — the one you configured, or a routed GPT based on prompt complexity.
- Loop — send messages to the LLM. If it requests a tool, validate args, optionally require approval, execute, send the result back. Repeat until a final answer or
maxTurns/ timeout. - Output guardrails — block leaks; optionally validate JSON against a Zod schema (and repair).
- Persist — write memory, append the session, queue graph extraction. Graph workers run in the background so this step does not wait on them.
You always get a RunResult: text (and optional parsed JSON), which agent finished, model used, token usage, traces, and events.
Quick start by feature
1. Define an agent
What: Name, instructions, and a model (or leave the model off and let Shirube route).
Use when: Every product surface — support bot, CLI assistant, Slack worker.
const agent = Agent.builder()
.name("support")
.instructions("You help customers. Prefer tools over guessing.")
.apiKey(process.env.OPENAI_API_KEY!)
// .model("gpt-4.1") // optional — omit to auto-pick by complexity
.build();
await agent.run("Where is my order?");
await run(agent, "Where is my order?"); // same thing, function style
Agent.create({ name, instructions, apiKey, ... }) accepts the same fields as a config object.
2. Tools
What: Functions the model can call. Each tool has a name, description, Zod input schema, and execute. Inputs are validated before execute. execute may be async.
Use when: The agent must hit your APIs (search, refund, calendar) instead of inventing answers.
const refund = tool({
name: "refund",
description: "Refund an order by id. Only after the user confirms.",
parameters: z.object({ orderId: z.string() }),
requireApproval: true, // fail-closed unless run() provides approval
execute: async ({ input, context }) =>
payments.refund(input.orderId, context.userId),
});
Agent.builder().tools([refund]) /* or .tool(refund) */;
await agent.run("Refund order_9", {
userId: "cus_123",
approval: async ({ tool: name }) => name !== "refund" || user.isAdmin,
});
If a tool throws, the loop does not crash: the model receives { error: "..." } and can recover. Unknown / invalid JSON args raise ToolError.
3. Model routing and providers
What: If you skip .model(), a classifier labels the prompt simple | moderate | complex | reasoning and picks a GPT from a catalog. You can also swap OpenAI for Claude or Gemini, and chain fallbacks.
Use when: You want cheap models on “what’s 2+2” and a stronger model on architecture questions; or you cannot go down when OpenAI 5xxs.
| Tier | Default model |
|---|---|
| simple | gpt-4.1-mini |
| moderate | gpt-4.1 |
| complex / reasoning | gpt-5 |
.modelRouter({
mode: "hybrid", // skip the classifier call on obviously short prompts
catalog: {
classifier: "gpt-4.1-mini",
simple: "gpt-4.1-mini",
moderate: "gpt-4.1",
complex: "gpt-5",
reasoning: "gpt-5",
},
})
.provider(new OpenAIProvider({ apiKey }))
.fallback(new AnthropicProvider({ apiKey: process.env.ANTHROPIC_API_KEY }))
// also: GeminiProvider
4. Memory and sessions
Shirube keeps four layers separate on purpose:
| Layer | Lifetime | Example |
|---|---|---|
| Agent config | Process | Instructions, tools, rails |
| Run state | One run() |
Messages, usage, handoff hops |
| Session | One conversation | sessionId or FileSession — “what did we just say?” |
| Long-term memory | User / org | Mem0 or in-memory search — “Ada prefers sci-fi” |
| Graph | User / org | People, projects, WORKS_ON edges — structured knowledge |
Use sessions for chat UIs (turn N needs turn N-1). Use memory so next week’s ticket still knows the customer. Use the graph when you care about relationships, not just similar text.
.memory({ provider: "in-memory" })
.memory({ provider: "mem0", apiKey: process.env.MEM0_API_KEY })
.memory({ provider: "custom", instance: myProvider }) // search + add
.session(new FileSession("chat_9", "./.shirube/chat_9.json"))
await agent.run("Hello again", { userId: "ada", sessionId: "chat_9" });
userId scopes memory. Implement MemoryProvider to plug in Redis, pgvector, Zep, etc.
5. Graph memory (and three background workers)
What: An embedded property graph (nodes + typed edges). After each run, Shirube queues the conversation. Three independent timers — not the request thread — extract entities, build relationships, and clean the graph. On the next run, relevant facts are injected into the system prompt.
Use when: Support/ops agents that should remember “Ada works on Shirube, uses TypeScript” without you writing ETL.
import { Agent, graph, createFileGraph } from "shirube-ai";
graph.start(); // extract + relate + maintain
const agent = Agent.builder()
.name("ops")
.instructions("Use known facts about this user. Do not invent employers or projects.")
.apiKey(process.env.OPENAI_API_KEY!)
.graph(true)
.build();
await agent.run("I'm Ada. I work on Project Shirube using TypeScript.", {
userId: "ada",
});
await graph.flush(); // tests / shutdown only — production uses intervals
console.log(graph.contextFor("Shirube", "ada"));
| Worker | Job |
|---|---|
| Extract | People, projects, technologies, preferences, facts from the transcript |
| Relate | Edges such as WORKS_ON, USES, PREFERS — same pair updates confidence instead of duplicating |
| Maintain | Merge duplicate nodes, decay stale edges, prune low-confidence noise |
Persist with createFileGraph("./.shirube-graph.json") or pass your own GraphRuntime. Failures retry up to 3 times and are counted on graph.stats.
6. Handoffs (multi-agent)
What: Agent A exposes transfer_to_<name> tools. The model can delegate. Messages stay; the specialist continues the same run. Shirube blocks A→B→A loops and caps hops (maxHandoffs, default 4).
Use when: A triage bot should not answer invoices; a billing specialist should.
const billing = Agent.builder()
.name("billing")
.instructions("You only resolve invoices and charges.")
.apiKey(key)
.build();
const triage = Agent.builder()
.name("triage")
.instructions("Route billing to the billing agent. Handle everything else yourself.")
.apiKey(key)
.handoffs([billing])
.maxHandoffs(3)
.build();
const result = await triage.run("Why was I charged twice?");
result.handoffs; // ["triage->billing"]
result.agentName; // "billing"
7. Guardrails and approvals
What: Checks on input, output, and tool arguments/results. Built-in rails cover jailbreak, prompt injection, PII redaction, secret leak, system-prompt leak, and max input size. You add domain rails (no legal advice, no SSN).
Use when: The agent is public-facing or can call write APIs.
.security({ jailbreak: true, pii: { redact: true } })
.security(false) // opt out of builtins; custom rails still work
const noLegal: Guardrail = {
name: "no_legal_advice",
stage: "output",
run: ({ text }) => {
const hit = /sue|lawsuit/i.test(text);
return { action: hit ? "block" : "allow", tripwireTriggered: hit };
},
};
.inputGuardrails([]).outputGuardrails([noLegal]).toolGuardrails([])
block + tripwire → GuardrailTripwireError. redact rewrites text and continues.
8. Structured output
What: .output(zodSchema) asks for JSON, parses it, and retries a repair turn if validation fails.
Use when: You need { label, confidence } for a ticketing pipeline, not free-form chat.
const Ticket = z.object({
label: z.enum(["bug", "billing", "other"]),
confidence: z.number(),
});
const agent = Agent.builder()
.name("classifier")
.instructions("Classify the ticket.")
.output(Ticket)
.build();
const result = await agent.run("The app crashes on launch.");
result.outputParsed; // { label: "bug", confidence: 0.9 }
After the retry budget it throws OutputValidationError.
9. Streaming and events
What: runStream() is an async iterator of runtime events. run({ onEvent }) is the callback form. Same events: text, tools, handoffs, guardrails, memory, graph, completion.
Use when: You are driving a UI, a terminal spinner, or logging.
for await (const event of agent.runStream("Hello")) {
if (event.type === "text.streamed") {
process.stdout.write(String(event.data?.delta ?? ""));
}
if (event.type === "tool.started") {
console.log("calling", event.data?.name);
}
}
Event types: run.started, run.completed, run.failed, text.streamed, tool.started, tool.completed, handoff.started, guardrail.triggered, memory.updated, graph.updated, model.called.
10. Tracing, retries, timeouts
What: Every run has a runId. Traces record model calls, tools, handoffs, memory/graph I/O, timing, and errors. Retries and timeouts wrap provider calls.
Use when: You need to debug “why did this cost 12 turns?” or survive flaky APIs.
.retry({ maxAttempts: 3, baseDelayMs: 200 })
.timeout(60_000)
.tracing({ enabled: true, onEvent: (e) => logger.debug(e) })
result.traces;
result.usage; // { inputTokens, outputTokens, totalTokens }
result.durationMs;
Cancel with abortSignal. Errors are typed: ConfigError, GuardrailTripwireError, MaxTurnsError, TimeoutError, ToolError, ModelError, MemoryError, McpError, OutputValidationError, AbortError.
11. MCP (Model Context Protocol)
What: Attach external MCP servers (GitHub, Linear) as tools, and serve your internal tools so Cursor or Claude Desktop can call them. Configure once; every agent inherits unless .mcp(false).
Use when: The agent should use existing MCP servers, or you want one catalog of company tools for both Shirube agents and IDEs.
import { mcp, tool } from "shirube-ai";
mcp.configure({
internal: [refund],
external: [
{
name: "github",
command: "npx",
args: ["-y", "@modelcontextprotocol/server-github"],
env: { GITHUB_TOKEN: process.env.GITHUB_TOKEN! },
},
{ name: "linear", url: "https://mcp.linear.app/mcp" },
],
});
Agent.builder().mcp() // or .mcp({ external: ["github"], include: ["github__*"] })
await mcp.serve({ transport: "stdio" });
await mcp.serve({ transport: "http", port: 3333, path: "/mcp" });
External tools are namespaced (github__create_issue). agent.run() connects lazily; await agent.connect() fails fast; await agent.close() stops stdio servers. Local .tools() win on name clashes.
Builder reference
Agent.builder()
.name("ops")
.instructions("You operate internal tools.") // string or (ctx) => string
.apiKey(process.env.OPENAI_API_KEY!)
.baseURL(process.env.OPENAI_BASE_URL)
.model("gpt-4.1")
.modelRouter({ mode: "hybrid" })
.provider(openai)
.fallback(anthropic)
.tools([search])
.handoffs([billing])
.maxHandoffs(3)
.output(schema)
.graph(true)
.mcp()
.memory({ provider: "in-memory" })
.session(session)
.security({ pii: { redact: true } })
.inputGuardrails([]).outputGuardrails([]).toolGuardrails([])
.maxTurns(8)
.timeout(60_000)
.retry({ maxAttempts: 3 })
.temperature(0.2)
.maxTokens(2048)
.tracing({ enabled: true })
.use(metricsPlugin)
.feature("tenant", "acme")
.build();
Plugins hook onBuild, onBeforeRun, onAfterModel, onAfterTool, onAfterRun, onError.
Run result
const result = await agent.run("Summarize the latest invoice", {
userId: "cus_123",
sessionId: "chat_9",
metadata: { ticket: "T-204" },
abortSignal: controller.signal,
onEvent: (e) => {},
approval: async () => true,
});
result.output;
result.outputParsed; // if .output(schema)
result.agentName;
result.handoffs;
result.model;
result.complexity; // when routing ran
result.usage;
result.turns;
result.durationMs;
result.runId;
result.guardrails;
result.traces;
result.events;
result.messages;
Examples in this repo
| File | Shows |
|---|---|
examples/basic.ts |
Tool + in-memory memory |
examples/graph.ts |
Graph workers + retrieval |
npx tsx examples/basic.ts
License
MIT