Quickstart Tutorial

From zero to a working agent in minutes. Follow along in TypeScript or Python.

  1. 1. Install
  2. 2. Verify
  3. 3. CLI Basics
  4. 4. Memory
  5. 5. Create Agent
  6. 6. Test It
  7. 7. Data Sloshing
  8. 8. Chain Agents
  9. 9. Multi-Agent
1

Install

There are three ways to get openrappter running. Pick whichever fits your workflow.

Option A — one-line installer (recommended):

curl -fsSL https://kody-w.github.io/openrappter/install.sh | bash

The script detects your platform, clones the repo, installs dependencies, builds the TypeScript runtime, and adds openrappter to your PATH.

Option B — manual clone:

# TypeScript runtime
git clone https://github.com/kody-w/openrappter.git
cd openrappter/typescript
npm install
npm run build

# Python runtime (optional, mirrors TypeScript)
cd ../python
pip install -e .

Option C — teach your existing agent: If you already have an AI assistant, paste the skills.md link into your conversation and ask it to set up openrappter for you. It can handle the full install and scaffold new agents.

Prerequisites: Node.js 18+ is required for the TypeScript runtime. Python 3.10+ is required for the Python runtime. Both runtimes implement the same agent contract and are interchangeable.
2

Verify

Confirm the installation is healthy before writing any code.

# Check version — should print v1.9.1
openrappter --version
openrappter --version
$ openrappter --version
openrappter v1.9.1
# List all auto-discovered agents
openrappter --list-agents
openrappter --list-agents
$ openrappter --list-agents
Available agents (3):
BasicAgent — Abstract base with data sloshing
ShellAgent — Shell commands, file read/write/list
MemoryAgent — Memory storage and retrieval

If you see agents listed, the runtime discovered your src/agents/ (TypeScript) or python/openrappter/agents/ (Python) directory successfully. Any custom agents you add there will appear here automatically.

3

CLI Basics

openrappter has three primary usage patterns from the command line.

Interactive REPL mode — drop into a persistent session where the agent remembers context across messages:

openrappter

Single task mode — run one task and exit, useful for scripting:

openrappter --task "list files in current directory"

Specify an agent directly — bypass the router and run a named agent:

openrappter --agent shell "what's in my home directory?"
openrappter --exec ShellAgent "read package.json"
single task example
$ openrappter --task "list files in current directory"
Executing ShellAgent...
README.md package.json src/ dist/ node_modules/
Done.

Other useful flags: --status shows loaded agents and runtime health, --version prints the version, and --help lists all available options.

4

Memory

openrappter has first-class persistent memory that survives across sessions. Anything you store is written locally and recalled automatically in future queries.

Store a fact:

openrappter "remember that I prefer dark mode"
storing a memory
$ openrappter "remember that I prefer dark mode"
Got it. I'll remember: "I prefer dark mode" (id: a3f1c9)

Recall facts:

openrappter "what do you know about my preferences?"
recalling memory
$ openrappter "what do you know about my preferences?"
Here's what I remember about your preferences:
- I prefer dark mode (stored 2 minutes ago)

Memory entries are stored at ~/.openrappter/memory.json as structured JSON. Each entry contains an ID, content, tags, created timestamp, and an access counter that helps the agent prioritize frequently recalled facts.

Local-first: All memory is stored on your machine. No data leaves your system. You can inspect, back up, or wipe the file at any time with cat ~/.openrappter/memory.json or rm ~/.openrappter/memory.json.
5

Create a Custom Agent

Every agent in openrappter follows the single-file pattern: one file, one agent. The metadata contract, documentation, and implementation all live together using native language constructs — no YAML, no config files, no magic parsing.

Create a GreeterAgent that returns a time-aware greeting and passes data downstream via data_slush.

// typescript/src/agents/GreeterAgent.ts
import { BasicAgent } from './BasicAgent.js';
import type { AgentMetadata } from './types.js';

export class GreeterAgent extends BasicAgent {
  constructor() {
    const metadata: AgentMetadata = {
      name: 'GreeterAgent',
      description: 'Greets users by name with a fun fact',
      parameters: {
        type: 'object',
        properties: {
          name: { type: 'string', description: 'Name to greet' }
        },
        required: ['name']
      }
    };
    super('GreeterAgent', metadata);
  }

  async perform(kwargs: Record<string, unknown>) {
    const name = kwargs.name as string;
    const hour = new Date().getHours();
    const greeting = hour < 12 ? 'Good morning' : hour < 18 ? 'Good afternoon' : 'Good evening';
    return {
      message: `${greeting}, ${name}!`,
      data_slush: { greeted_user: name, time_of_day: greeting }
    };
  }
}
# python/openrappter/agents/greeter_agent.py
from openrappter.agents.basic_agent import BasicAgent
from datetime import datetime

class GreeterAgent(BasicAgent):
    def __init__(self):
        self.name = 'GreeterAgent'
        self.metadata = {
            "name": self.name,
            "description": "Greets users by name with a fun fact",
            "parameters": {
                "type": "object",
                "properties": {
                    "name": {"type": "string", "description": "Name to greet"}
                },
                "required": ["name"]
            }
        }
        super().__init__(name=self.name, metadata=self.metadata)

    def perform(self, **kwargs):
        name = kwargs.get('name', 'World')
        hour = datetime.now().hour
        greeting = 'Good morning' if hour < 12 else 'Good afternoon' if hour < 18 else 'Good evening'
        return {
            "message": f"{greeting}, {name}!",
            "data_slush": {"greeted_user": name, "time_of_day": greeting}
        }

Drop the file into the agents directory. The framework auto-discovers all files matching *Agent.ts (TypeScript) or *_agent.py (Python) — no registration step required.

6

Test It

After saving the file, confirm the agent is discovered and invoke it directly by name.

# Verify it was discovered
openrappter --list-agents
# Should now include GreeterAgent in the list

# Invoke it directly
openrappter --agent greeter "greet Alice"
invoking GreeterAgent
$ openrappter --agent greeter "greet Alice"
Executing GreeterAgent...
Data sloshing: injecting temporal + memory context
Agent response:
{
"message": "Good afternoon, Alice!",
"data_slush": {
"greeted_user": "Alice",
"time_of_day": "Good afternoon"
}
}

The greeting matches the current time of day because the perform() method reads the system clock. In the next step, you will see how to get this and much richer context from the data sloshing pipeline instead.

You can also write a Vitest test alongside the agent (TypeScript) or a pytest file (Python) to validate behavior in CI:

// typescript/src/__tests__/GreeterAgent.test.ts
import { describe, it, expect } from 'vitest';
import { GreeterAgent } from '../agents/GreeterAgent.js';

describe('GreeterAgent', () => {
  it('returns a greeting with the provided name', async () => {
    const agent = new GreeterAgent();
    const result = await agent.perform({ name: 'Alice' });
    expect(result.message).toContain('Alice');
    expect(result.data_slush.greeted_user).toBe('Alice');
  });
});
7

Data Sloshing

Before perform() is ever called, the framework automatically gathers context signals and synthesizes an orientation for your agent. This is data sloshing — implicit context enrichment that happens on every execution without any work from you.

The signals are accessible in perform() via getSignal() (TypeScript) or get_signal() (Python) using dot-notation paths:

async perform(kwargs: Record<string, unknown>) {
  // Temporal awareness — injected before perform() runs
  const timeOfDay    = this.getSignal<string>('temporal.time_of_day', 'morning');
  const isWeekend    = this.getSignal<boolean>('temporal.is_weekend', false);
  const fiscalPeriod = this.getSignal<string>('temporal.fiscal_period');

  // Synthesized orientation — how confident is the agent?
  const confidence = this.getSignal<string>('orientation.confidence', 'medium');
  const approach   = this.getSignal<string>('orientation.approach');
  if (confidence === 'low') {
    return { status: 'clarify', message: 'Could you be more specific?' };
  }

  // Query signals — understand what the user is asking
  const isQuestion  = this.getSignal<boolean>('query_signals.is_question', false);
  const specificity = this.getSignal<string>('query_signals.specificity');

  // Memory echoes — recent relevant memories surfaced automatically
  const echoes = this.getSignal<any[]>('memory_echoes', []);
  if (echoes.length > 0) {
    const lastEcho = echoes[0];  // most relevant past interaction
  }

  // Upstream slush — curated signals from the previous agent in a chain
  const upstream = this.context.upstream_slush ?? {};
}
def perform(self, **kwargs):
    # Temporal awareness — injected before perform() runs
    time_of_day    = self.get_signal('temporal.time_of_day', 'morning')
    is_weekend     = self.get_signal('temporal.is_weekend', False)
    fiscal_period  = self.get_signal('temporal.fiscal_period')

    # Synthesized orientation — how confident is the agent?
    confidence = self.get_signal('orientation.confidence', 'medium')
    approach   = self.get_signal('orientation.approach')
    if confidence == 'low':
        return {"status": "clarify", "message": "Could you be more specific?"}

    # Query signals — understand what the user is asking
    is_question  = self.get_signal('query_signals.is_question', False)
    specificity  = self.get_signal('query_signals.specificity')

    # Memory echoes — recent relevant memories surfaced automatically
    echoes = self.get_signal('memory_echoes', [])
    if echoes:
        last_echo = echoes[0]  # most relevant past interaction

    # Upstream slush — curated signals from the previous agent in a chain
    upstream = self.context.get('upstream_slush', {})

All available signal namespaces: temporal.*, memory_echoes, query_signals.*, behavioral_hints.*, orientation.*, and upstream_slush.*. See the Architecture page for the full signal reference.

Data Slush: When your agent returns a data_slush key in its output, the framework extracts those values and stores them in lastDataSlush (TypeScript) or last_data_slush (Python). The next agent in the chain can receive this as upstream_slush — enabling LLM-free, deterministic agent-to-agent pipelines.
8

Chain Agents

Agent chaining lets you build pipelines where one agent's output becomes the next agent's context. Agent A returns data_slush containing curated signals; Agent B receives that as upstream_slush via its execute() call.

import { GreeterAgent } from './agents/GreeterAgent.js';
import { FollowUpAgent } from './agents/FollowUpAgent.js';

// Step 1: Agent A runs and populates lastDataSlush
const greeter = new GreeterAgent();
await greeter.execute({ query: 'greet Alice', name: 'Alice' });

// greeter.lastDataSlush is now:
// { greeted_user: "Alice", time_of_day: "Good afternoon" }

// Step 2: Agent B receives A's slush as upstream context
const followUp = new FollowUpAgent();
const result = await followUp.execute({
  query: 'suggest something for Alice',
  upstream_slush: greeter.lastDataSlush   // pass A's slush to B
});

// Inside FollowUpAgent.perform():
// const greetedUser = this.getSignal('upstream_slush.greeted_user');
// const timeOfDay   = this.getSignal('upstream_slush.time_of_day');
from openrappter.agents.greeter_agent import GreeterAgent
from openrappter.agents.follow_up_agent import FollowUpAgent

# Step 1: Agent A runs and populates last_data_slush
greeter = GreeterAgent()
greeter.execute(query='greet Alice', name='Alice')

# greeter.last_data_slush is now:
# {"greeted_user": "Alice", "time_of_day": "Good afternoon"}

# Step 2: Agent B receives A's slush as upstream context
follow_up = FollowUpAgent()
result = follow_up.execute(
    query='suggest something for Alice',
    upstream_slush=greeter.last_data_slush   # pass A's slush to B
)

# Inside FollowUpAgent.perform():
# greeted_user = self.get_signal('upstream_slush.greeted_user')
# time_of_day  = self.get_signal('upstream_slush.time_of_day')

This pattern is the foundation of LLM-free agent pipelines. Agent A extracts structured facts; Agent B reasons from them — no large language model required in the middle. The data_slush key acts as a typed handoff contract between agents.

For more complex graphs — fan-out, fan-in, conditional routing — see the multi-agent patterns in the next step.

9

Multi-Agent Patterns

openrappter ships three built-in orchestration primitives for coordinating multiple agents: BroadcastManager, AgentRouter, and SubAgent. These cover the vast majority of multi-agent topologies.

BroadcastManager — send a message to multiple agents simultaneously and collect results:

import { BroadcastManager } from './agents/broadcast.js';
import { GreeterAgent } from './agents/GreeterAgent.js';
import { ShellAgent } from './agents/ShellAgent.js';

// 'all' — wait for every agent to complete
const broadcast = new BroadcastManager([new GreeterAgent(), new ShellAgent()], 'all');
const results = await broadcast.execute({ query: 'hello' });

// 'race' — take the first agent to respond
const race = new BroadcastManager([agentA, agentB, agentC], 'race');
const first = await race.execute({ query: 'who can answer this fastest?' });

// 'fallback' — try agents in order until one succeeds
const fallback = new BroadcastManager([primaryAgent, backupAgent], 'fallback');
const safe = await fallback.execute({ query: 'try primary, then backup' });
# Python BroadcastManager mirrors the TypeScript API
from openrappter.agents.broadcast import BroadcastManager
from openrappter.agents.greeter_agent import GreeterAgent
from openrappter.agents.shell_agent import ShellAgent

# 'all' — wait for every agent to complete
broadcast = BroadcastManager([GreeterAgent(), ShellAgent()], mode='all')
results = broadcast.execute(query='hello')

# 'race' — take the first agent to respond
race = BroadcastManager([agent_a, agent_b, agent_c], mode='race')
first = race.execute(query='who can answer this fastest?')

# 'fallback' — try agents in order until one succeeds
fallback = BroadcastManager([primary_agent, backup_agent], mode='fallback')
safe = fallback.execute(query='try primary, then backup')

AgentRouter — route messages to different agents based on rules (sender, channel, group, or pattern matching), with priority and session key isolation:

import { AgentRouter } from './agents/router.js';

const router = new AgentRouter();
router.addRule({ pattern: /^greet/i, agent: new GreeterAgent(), priority: 10 });
router.addRule({ pattern: /^run|^exec/i, agent: new ShellAgent(), priority: 5 });
router.addRule({ channel: 'slack', agent: new SlackAgent(), priority: 1 });

const result = await router.route({ query: 'greet the team', channel: 'cli' });

SubAgent — invoke nested agents from within a parent agent's perform() method, with configurable depth limits and built-in loop detection to prevent infinite recursion:

import { SubAgent } from './agents/subagent.js';

// Inside a parent agent's perform():
const sub = new SubAgent(new ShellAgent(), { maxDepth: 3 });
const fileList = await sub.invoke({ action: 'list', path: '.' });
Next: The Architecture page covers the full execution flow, all sloshing signal namespaces, the WebSocket gateway, multi-channel routing (Slack, Discord, Telegram, iMessage, Teams), and the ClawHub skills system in depth.
Next Steps