Back to skills
extension
Category: Content & MediaNo API key required

sisu-framework

Build AI agents using the Sisu TypeScript framework. Use when creating agents, implementing middleware, defining tools with Zod schemas, setting up LLM adapters (OpenAI, Anthropic, Ollama), or working with agent control flow, tracing, and observability.

personAuthor: jakexiaohubgithub

Sisu Framework

Build reliable AI agents in TypeScript with full transparency and control.

When to use this skill

  • Creating new AI agents or agent pipelines
  • Implementing tool calling with LLM models
  • Setting up middleware for control flow, error handling, or tracing
  • Working with multiple LLM providers (OpenAI, Anthropic, Ollama)
  • Debugging agent behavior with trace viewers
  • Building RAG (Retrieval Augmented Generation) systems

Discovery-first rule

Before inventing new Sisu middleware, tools, adapters, or RAG primitives:

  1. check the maintained package surface first
  2. prefer composition of existing packages over custom framework code
  3. only propose new middleware/tools when an existing maintained package truly does not fit

If the sisu CLI is available, use it first:

sisu list middleware
sisu list tools
sisu list adapters
sisu list vector
sisu info mw-rag
sisu info tool-rag

If sisu is not installed, use npx equivalents:

npx @sisu-ai/cli list middleware
npx @sisu-ai/cli list tools
npx @sisu-ai/cli info mw-rag

If the CLI is not available, inspect:

  • installed package READMEs in node_modules/@sisu-ai/*
  • official package docs on npm/GitHub
  • agent-friendly docs: http://sisuai.me/agents.html
  • the public examples in https://github.com/finger-gun/sisu/tree/main/examples

Do not invent ad-hoc rag(...), custom vector abstractions, or custom tool registries when maintained Sisu packages already exist.

Quick start

Installation

pnpm add @sisu-ai/core @sisu-ai/adapter-openai \
         @sisu-ai/mw-register-tools \
         @sisu-ai/mw-conversation-buffer @sisu-ai/mw-trace-viewer \
         @sisu-ai/mw-error-boundary zod dotenv

Basic agent template

import "dotenv/config";
import { Agent, createCtx, execute, type Tool } from "@sisu-ai/core";
import { registerTools } from "@sisu-ai/mw-register-tools";
import { inputToMessage, conversationBuffer } from "@sisu-ai/mw-conversation-buffer";
import { errorBoundary } from "@sisu-ai/mw-error-boundary";
import { openAIAdapter } from "@sisu-ai/adapter-openai";
import { traceViewer } from "@sisu-ai/mw-trace-viewer";
import { z } from "zod";

// Create context
const ctx = createCtx({
  model: openAIAdapter({ model: "gpt-5.4" }),
  input: "User input here",
  systemPrompt: "You are a helpful assistant.",
});

// Build pipeline
const app = new Agent()
  .use(errorBoundary())
  .use(traceViewer())
  .use(registerTools([...]))  // Add tools here
  .use(inputToMessage)
  .use(conversationBuffer({ window: 8 }))
  .use(execute);

// Run
await app.handler()(ctx);

Core concepts

Context (Ctx)

Everything flows through a single typed context object. Never create hidden state.

Key properties:

  • input - User input string
  • messages - Conversation history
  • model - LLM adapter
  • tools - Tool registry
  • memory - Key-value storage
  • state - Middleware state
  • signal - AbortSignal for cancellation
  • log - Logger

Middleware pattern

Middleware signature: (ctx, next) => Promise<void>

Critical rules:

  • Always await next() unless short-circuiting
  • Don't mutate unrelated ctx properties
  • Propagate ctx.signal to all async operations
const myMiddleware = async (ctx, next) => {
  // Before logic
  ctx.log.info("Starting");

  await next(); // MUST call this

  // After logic
  ctx.log.info("Finished");
};

Tools with Zod validation

import { z } from "zod";
import type { Tool } from "@sisu-ai/core";

const myTool: Tool<{ city: string }> = {
  name: "toolName",
  description: "Clear description for the LLM",
  schema: z.object({
    city: z.string().min(1),
  }),
  handler: async ({ city }, ctx) => {
    // ctx is sandboxed - has memory, signal, log, model, deps
    // Cannot access: tools, messages, state, input, stream
    return { result: "data" };
  },
};

Common patterns

Simple chat agent

const app = new Agent()
  .use(errorBoundary())
  .use(traceViewer())
  .use(inputToMessage)
  .use(conversationBuffer({ window: 8 }))
  .use(async (ctx) => {
    const res = await ctx.model.generate(ctx.messages, {
      toolChoice: "none",
      signal: ctx.signal,
    });
    if (res?.message) ctx.messages.push(res.message);
  });

Tool-calling agent

const app = new Agent()
  .use(errorBoundary())
  .use(traceViewer())
  .use(registerTools([tool1, tool2]))
  .use(inputToMessage)
  .use(conversationBuffer({ window: 8 }))
  .use(execute);

Agent with control flow

See CONTROL_FLOW.md for branching, looping, and parallel execution.

RAG agent

See RAG.md for retrieval augmented generation patterns.

LLM adapters

OpenAI

import { openAIAdapter } from "@sisu-ai/adapter-openai";

// Standard OpenAI
const model = openAIAdapter({ model: "gpt-5.4" });

// Compatible APIs (LM Studio, vLLM, OpenRouter)
const model = openAIAdapter({
  model: "gpt-5.4",
  baseUrl: "http://localhost:1234/v1",
});

Anthropic

import { anthropicAdapter } from "@sisu-ai/adapter-anthropic";
const model = anthropicAdapter({ model: "claude-sonnet-4" });

Ollama (local)

import { ollamaAdapter } from "@sisu-ai/adapter-ollama";
const model = ollamaAdapter({ model: "llama3.1" });

Essential middleware

Control flow

import { sequence, branch, switchCase, loopUntil } from '@sisu-ai/mw-control-flow';

// Sequential steps
.use(sequence([step1, step2, step3]))

// Conditional branching
.use(branch(
  ctx => /weather/.test(ctx.input ?? ''),
  toolPipeline,
  chatPipeline
))

// Route by intent
.use(switchCase(
  ctx => ctx.state.intent,
  { 'search': searchFlow, 'chat': chatFlow }
))

Safety and validation

import { guardrails } from '@sisu-ai/mw-guardrails';
import { invariants } from '@sisu-ai/mw-invariants';

.use(errorBoundary())  // Always first
.use(guardrails({
  maxTokens: 2000,
  timeout: 30000
}))
.use(invariants())  // Development mode validation

Observability

import { traceViewer } from '@sisu-ai/mw-trace-viewer';
import { usageTracker } from '@sisu-ai/mw-usage-tracker';

.use(traceViewer())   // Generates HTML traces
.use(usageTracker())  // Tracks costs

Error handling

import {
  isSisuError,
  getErrorDetails,
  ToolExecutionError,
  ValidationError,
} from "@sisu-ai/core";

try {
  await app.handler()(ctx);
} catch (err) {
  if (isSisuError(err)) {
    console.error("Code:", err.code);
    console.error("Context:", err.context);
  } else {
    console.error(getErrorDetails(err));
  }
}

Use errorBoundary middleware:

.use(errorBoundary(async (err, ctx) => {
  ctx.log.error('Error:', getErrorDetails(err));
  ctx.messages.push({
    role: 'assistant',
    content: 'I encountered an error.'
  });
}))

Environment variables

# Required for examples
OPENAI_API_KEY=sk-...
ANTHROPIC_API_KEY=sk-ant-...

# Optional
LOG_LEVEL=info           # debug|info|warn|error
TRACE_HTML=1            # Generate HTML traces
TRACE_STYLE=dark        # light|dark

Best practices

  1. Always use errorBoundary as first middleware
  2. Enable traceViewer during development (second middleware)
  3. Validate tool inputs with Zod schemas
  4. Use conversationBuffer to prevent context overflow
  5. Propagate ctx.signal to all async operations
  6. Keep middleware small - one responsibility each
  7. Use control flow combinators over complex conditionals
  8. Never log secrets - use createRedactingLogger
  9. Set guardrails in production (maxTokens, timeout)
  10. Test with AbortSignal for cancellation

Common mistakes

❌ Not calling next()

// WRONG
const bad = async (ctx, next) => {
  ctx.state.value = 1;
  // Missing await next()!
};

❌ Not propagating signal

// WRONG
const res = await ctx.model.generate(ctx.messages, {});

// CORRECT
const res = await ctx.model.generate(ctx.messages, {
  signal: ctx.signal,
});

❌ Mutating other middleware state

// WRONG - don't touch other middleware state
ctx.state.otherMiddlewareData = modified;

// CORRECT - namespace your state
ctx.state.myFeature = { myData: value };

❌ Using console.log

// WRONG
console.log("debug info");

// CORRECT
ctx.log.info("debug info");

Reference documentation

For detailed documentation, see:

External resources