Skip to main content

Documentation Index

Fetch the complete documentation index at: https://mintlify.com/bradygaster/squad/llms.txt

Use this file to discover all available pages before exploring further.

Beyond the basics of casting and onboarding, Squad supports sophisticated multi-agent patterns ready for production — autonomous task pipelines, cost-aware model routing, governance hooks for PII and file guards, real-time token streaming, and pluggable storage backends that swap the local filesystem for Azure Blob Storage or SQLite. The samples in this section are working TypeScript programs you can run today.

Autonomous pipeline

The autonomous-pipeline sample is the comprehensive showcase of the Squad runtime. Four agents — Keyser (lead), McManus (developer), Fenster (tester), and Verbal (scribe) — autonomously pick up tasks from a queue, route follow-up work to teammates, record architectural decisions, and accumulate learnings, all while CostTracker and TelemetryCollector monitor the run.
import {
  CastingEngine,
  CostTracker,
  TelemetryCollector,
  SkillRegistry,
  selectResponseTier,
  StreamingPipeline,
  initSquadTelemetry,
  recordAgentSpawn,
  recordAgentDuration,
  recordTokenUsage,
} from '@bradygaster/squad-sdk';

// Cast the team
const engine = new CastingEngine();
const cast = engine.castTeam({
  universe: 'usual-suspects',
  requiredRoles: ['lead', 'developer', 'tester', 'scribe'],
  teamSize: 4,
});

// Initialize monitoring components
const costTracker = new CostTracker();
const telemetry = new TelemetryCollector({ enabled: true });
const skillRegistry = new SkillRegistry();
const streaming = new StreamingPipeline();

// Build agent states with streaming sessions
const agents = cast.map((member, i) => {
  const sessionId = `session-${member.name.toLowerCase()}-${i}`;
  streaming.attachToSession(sessionId);
  recordAgentSpawn(member.name);
  return { member, status: 'idle', tasksCompleted: 0, sessionId };
});

// Autonomous execution loop — agents pick up role-matched tasks
while (tasks.some((t) => t.status !== 'done')) {
  for (const agent of agents) {
    const task = findNextTask(tasks, agent.member.role);
    if (!task) continue;

    // Select the cheapest model tier that fits the task
    const tier = selectResponseTier(task.description, config, tierContext);

    // Process streaming deltas
    streaming.markMessageStart(agent.sessionId);
    await streaming.processEvent({ type: 'message_delta', ... });

    // Record cost
    costTracker.recordUsage({
      sessionId: agent.sessionId,
      agentName: agent.member.name,
      model: tier.modelTier,
      inputTokens: cost.inputTokens,
      outputTokens: cost.outputTokens,
      estimatedCost: cost.cost,
    });

    // Agent coordination patterns
    maybeRouteFollowUp(task, agent, tasks, timeline);   // squad_route
    maybeRecordDecision(task, agent, decisions, timeline); // squad_decide
    maybeRecordMemory(task, agent, memories, timeline);  // squad_memory
  }
}
Use cases: CI/CD automation, automated code review pipelines, scheduled documentation passes, and any workflow where agents should coordinate without a human in the loop. Key coordination patterns demonstrated:
PatternWhat it does
squad_routeAn agent routes a follow-up task to a teammate (e.g., developer → tester after implementation)
squad_decideAn agent records an architectural decision to a shared log
squad_memoryAn agent persists a learning for future sessions
Set OTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:4317 before running to export traces and metrics to the .NET Aspire dashboard. The sample emits squad.agent.spawns, squad.agent.duration, squad.tokens.input, and squad.sessions.created metrics automatically.

Cost-aware routing

The cost-aware-router sample demonstrates how Squad selects the cheapest model tier that satisfies task complexity requirements, tracks per-agent spending, and fires budget warnings before limits are reached.
import {
  CostTracker,
  selectResponseTier,
  getTier,
  MODELS,
} from '@bradygaster/squad-sdk';
import type { ResponseTier, TierName, SquadConfig } from '@bradygaster/squad-sdk';

const config: SquadConfig = {
  version: '0.8.0',
  team: { name: 'MVP Summit Demo' },
  routing: {
    rules: [
      { pattern: 'typo', agents: ['Cheritto'], tier: 'direct' },
    ],
  },
  models: {
    default: MODELS.DEFAULT,
    defaultTier: 'standard',
    tiers: {
      premium: [...MODELS.FALLBACK_CHAINS.premium],
      standard: [...MODELS.FALLBACK_CHAINS.standard],
      fast: [...MODELS.FALLBACK_CHAINS.fast],
    },
  },
  agents: [
    { name: 'Cheritto', role: 'developer' },
    { name: 'McManus',  role: 'scribe' },
    { name: 'Verbal',   role: 'developer' },
    { name: 'Keaton',   role: 'lead' },
    { name: 'Fenster',  role: 'tester' },
  ],
};

const BUDGET_LIMIT = 0.50; // $0.50 demo budget
const tracker = new CostTracker();

for (const task of TASKS) {
  // Route to the right tier based on task description
  const tier: ResponseTier = selectResponseTier(task.message, config);

  // Record actual usage after the task completes
  tracker.recordUsage({
    sessionId: `session-${task.agent.toLowerCase()}`,
    agentName: task.agent,
    model: tier.modelTier,
    inputTokens: task.simInputTokens,
    outputTokens: task.simOutputTokens,
    estimatedCost: estimateCost(tier, task.simInputTokens, task.simOutputTokens),
  });

  // Check budget health
  const summary = tracker.getSummary();
  const pct = summary.totalEstimatedCost / BUDGET_LIMIT;
  if (pct >= 0.9) console.warn('⚠  BUDGET CRITICAL');
  else if (pct >= 0.7) console.warn('⚠  Budget warning');
}
How tier selection works: selectResponseTier() matches the task description against routing rules and complexity heuristics to pick one of four tiers:
TierModel classTypical use
directNo modelSimple cache lookups, trivial edits
lightweightFast / cheapDocs updates, brief summaries
standardBalancedFeature implementation, test writing
fullPremiumSecurity audits, architecture reviews
CostTracker.getSummary() returns a breakdown by agent and session, making it easy to build cost dashboards or enforce per-agent budgets programmatically.

Governance hooks

The hook-governance sample demonstrates HookPipeline — a composable middleware layer that enforces security and policy rules as deterministic TypeScript code rather than prompt instructions.
import {
  HookPipeline,
  ReviewerLockoutHook,
} from '@bradygaster/squad-sdk/hooks';
import type {
  PreToolUseContext,
  PostToolUseContext,
} from '@bradygaster/squad-sdk/hooks';

// ── 1. File-write guards ─────────────────────────────────────────────
const guardPipeline = new HookPipeline({
  allowedWritePaths: ['src/**/*.ts', '.squad/**'],
});

// Allowed: matches the glob pattern
const allowed = await guardPipeline.runPreToolHooks({
  toolName: 'create',
  arguments: { path: 'src/utils/helper.ts' },
  agentName: 'McManus',
  sessionId: 'session-001',
});
// → { action: 'allow' }

// Blocked: outside safe zones
const blocked = await guardPipeline.runPreToolHooks({
  toolName: 'edit',
  arguments: { path: '/etc/passwd' },
  agentName: 'McManus',
  sessionId: 'session-001',
});
// → { action: 'block', reason: '...' }

// ── 2. PII scrubbing ────────────────────────────────────────────────
const piiPipeline = new HookPipeline({ scrubPii: true });

const piiResult = await piiPipeline.runPostToolHooks({
  toolName: 'bash',
  arguments: { command: 'git log --oneline' },
  result: 'Deploy fix by brady@example.com — cc: alice@company.io',
  agentName: 'Verbal',
  sessionId: 'session-002',
});
// piiResult.result → 'Deploy fix by [EMAIL_REDACTED] — cc: [EMAIL_REDACTED]'

// ── 3. Reviewer lockout ──────────────────────────────────────────────
const lockoutPipeline = new HookPipeline({ reviewerLockout: true });
const lockout = lockoutPipeline.getReviewerLockout();

lockout.lockout('src/auth.ts', 'Backend');

const lockoutResult = await lockoutPipeline.runPreToolHooks({
  toolName: 'edit',
  arguments: { path: 'src/auth.ts' },
  agentName: 'Backend',
  sessionId: 'session-003',
});
// → { action: 'block', reason: '...' }

// ── 4. Ask-user rate limiting ────────────────────────────────────────
const ratePipeline = new HookPipeline({ maxAskUserPerSession: 3 });

for (let i = 1; i <= 5; i++) {
  const result = await ratePipeline.runPreToolHooks({
    toolName: 'ask_user',
    arguments: { question: `Clarification #${i}?` },
    agentName: 'Fenster',
    sessionId: 'session-004',
  });
  // Calls 1–3: { action: 'allow' }
  // Calls 4–5: { action: 'block', reason: '...' }
}
The four governance patterns:
HookWhat it guards
File-write guardBlocks writes to paths outside allowedWritePaths globs — prevents agents from touching .env, credentials, or system files
PII scrubbingRedacts email addresses (and other PII patterns) from PostToolUseContext.result before it reaches the agent
Reviewer lockoutAfter a reviewer rejects a change, the original author is locked out of that file — enforces code review decisions
Ask-user rate limitCaps the number of times an agent can interrupt the user per session — prevents noisy or runaway agents
Hooks run on every tool invocation and are applied in pipeline order. Keep hook logic lightweight and synchronous where possible to avoid adding latency to high-frequency tool calls.

Streaming

The streaming-chat sample shows how to route user messages to agents by keyword and surface token-by-token responses in real time. It uses StreamingPipeline together with SquadClientWithPool and EventBus.
import { CastingEngine, StreamingPipeline } from '@bradygaster/squad-sdk';
import type { StreamDelta } from '@bradygaster/squad-sdk';
import { SquadClientWithPool, EventBus } from '@bradygaster/squad-sdk/client';

// Set up the streaming pipeline — writes tokens to stdout as they arrive
const pipeline = new StreamingPipeline();
pipeline.onDelta((event) => {
  process.stdout.write(event.content);
});

// Create a session per agent and attach it to the pipeline
const client = new SquadClientWithPool({ pool: { maxConcurrent: 5 } });
await client.connect();

for (const agent of AGENTS) {
  const session = await client.createSession({
    streaming: true,
    systemMessage: { mode: 'append', content: agent.systemPrompt },
  });
  agent.sessionId = session.sessionId;
  pipeline.attachToSession(session.sessionId);
}

// Route a message and stream the response
function routeMessage(message: string): AgentInfo {
  const lower = message.toLowerCase();
  for (const agent of AGENTS) {
    if (agent.keywords.some((kw) => lower.includes(kw))) return agent;
  }
  return AGENTS[0]; // default to Backend
}

// Demo mode: simulate streaming without a live token
async function simulateStreaming(
  pipeline: StreamingPipeline,
  agent: AgentInfo,
): Promise<void> {
  const sessionId = agent.sessionId ?? `demo-${agent.role.toLowerCase()}`;
  const words = getDemoResponse(agent.role).split(' ');

  pipeline.markMessageStart(sessionId);

  for (let i = 0; i < words.length; i++) {
    const delta: StreamDelta = {
      type: 'message_delta',
      sessionId,
      agentName: agent.name,
      content: (i === 0 ? '' : ' ') + words[i],
      index: i,
      timestamp: new Date(),
    };
    await pipeline.processEvent(delta);
    await new Promise((r) => setTimeout(r, 40 + Math.random() * 40));
  }
}
Key streaming APIs:
APIDescription
pipeline.attachToSession(sessionId)Register a session so the pipeline routes its deltas to the right handlers
pipeline.onDelta(handler)Subscribe to every StreamDelta event across all attached sessions
pipeline.markMessageStart(sessionId)Signal the start of a new message — used to reset per-message state
pipeline.processEvent(event)Push a StreamDelta or UsageEvent into the pipeline
pipeline.detachFromSession(sessionId)Unsubscribe a session when it closes
pipeline.clear()Remove all sessions and reset internal state
Set SQUAD_DEMO_MODE=true to run streaming-chat without a GitHub Copilot token. The sample delivers pre-written word-by-word responses with realistic timing so you can explore the streaming API without any external dependencies.

Custom storage providers

Squad’s StorageProvider interface abstracts all file I/O behind an async contract. Two samples demonstrate full implementations:

Azure Blob Storage

AzureBlobStorageProvider maps the 24-method StorageProvider interface to Azure Blob Storage. Blobs map 1:1 to virtual paths inside a single container. Directories are virtual — they exist as common prefixes in blob names.
import { ContainerClient } from '@azure/storage-blob';
import type { StorageProvider, StorageStats } from '@bradygaster/squad-sdk';

export class AzureBlobStorageProvider implements StorageProvider {
  private readonly container: ContainerClient;

  constructor(containerClient: ContainerClient) {
    this.container = containerClient;
  }

  async read(filePath: string): Promise<string | undefined> {
    const blob = this.container.getBlockBlobClient(this.normalizePath(filePath));
    try {
      const response = await blob.download(0);
      const chunks: Buffer[] = [];
      for await (const chunk of response.readableStreamBody!) {
        chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
      }
      return Buffer.concat(chunks).toString('utf-8');
    } catch (err: any) {
      if (err.statusCode === 404) return undefined;
      throw err;
    }
  }

  async write(filePath: string, data: string): Promise<void> {
    const blob = this.container.getBlockBlobClient(this.normalizePath(filePath));
    const buffer = Buffer.from(data, 'utf-8');
    await blob.upload(buffer, buffer.length, {
      blobHTTPHeaders: { blobContentType: 'text/plain; charset=utf-8' },
    });
  }

  async append(filePath: string, data: string): Promise<void> {
    // Block Blobs have no native append — use read-modify-write
    const existing = (await this.read(filePath)) ?? '';
    await this.write(filePath, existing + data);
  }

  // rename() uses copy-then-delete (no atomic rename in Blob Storage)
  // list() uses listBlobsByHierarchy('/') for virtual directory traversal
  // Sync methods throw — cloud I/O is inherently async
}
When to use the Azure provider: multi-machine shared state, Azure Functions / Container Apps deployments, compliance requirements (RBAC, encryption at rest, audit logs). Run the sample with Azurite (no Azure account needed):
cd samples/storage-provider-azure
npm install
npm run azurite        # Terminal 1 — starts local emulator
npm run demo:emulator  # Terminal 2 — runs the demo

SQLite

SQLiteStorageProvider (exported directly from @bradygaster/squad-sdk) uses sql.js — SQLite compiled to WebAssembly — for a portable, zero-native-dependency storage backend. Files are rows in a files(path, content, updated_at) table with forward-slash paths as virtual keys.
import { SQLiteStorageProvider } from '@bradygaster/squad-sdk';

// Create and initialize the provider
const provider = new SQLiteStorageProvider('./squad-demo.db');
await provider.init(); // required — loads sql.js asynchronously

// Standard StorageProvider operations
await provider.write('team.md', '# Squad Team\n...');
await provider.write('agents/flight/charter.md', '# FLIGHT — Commander\n...');

const content = await provider.read('team.md');
// → '# Squad Team\n...'

const entries = await provider.list('agents/');
// → ['flight']

// Persistence: create a second instance from the same file
const provider2 = new SQLiteStorageProvider('./squad-demo.db');
await provider2.init();
const persisted = await provider2.read('team.md');
// → same content — SQLite persists across instances
Key patterns:
PatternWhy
await provider.init() is requiredsql.js loads the WASM engine asynchronously before any operation
Paths are virtual keysFiles don’t touch the filesystem — ideal for sandboxed environments
Atomic persistenceEvery write flushes the in-memory database to disk with write-then-rename for crash safety
No native binariessql.js WASM works on Windows, Linux, macOS, and macOS ARM without compilation
When to use SQLite: portable single-file storage, embedded apps, isolated test runs (swap in a fresh .db per test), environments with limited filesystem access.

Skill discovery

The skill-discovery sample shows how agents load domain knowledge from SKILL.md files, score them against task descriptions, and discover new patterns at runtime.
import {
  SkillRegistry,
  loadSkillsFromDirectory,
  parseSkillFile,
} from '@bradygaster/squad-sdk/skills';

// Load all SKILL.md files from a directory (synchronous)
const skills = loadSkillsFromDirectory(skillsDir);
const registry = new SkillRegistry();
for (const skill of skills) registry.registerSkill(skill);

// Match skills to a task — scored by trigger keywords + role affinity
const matches = registry.matchSkills(
  'Add TypeScript generics to the data layer',
  'developer',
);
// → [{ skill: { id: 'typescript-patterns', ... }, score: 0.8, reason: 'triggers: typescript, types; role affinity: developer' }]

// An agent discovers a new pattern at runtime
const newSkillContent = `---
name: Error Handling Patterns
domain: reliability
triggers: [error, exception, result, try, catch, handling]
roles: [developer, backend, tester]
confidence: low
---
## Error Handling Patterns

Use Result<T, E> type pattern instead of throwing exceptions.
Never swallow errors silently — at minimum, log and re-throw.
`;

const discovered = parseSkillFile('error-handling', newSkillContent);
if (discovered) registry.registerSkill(discovered);
The SKILL.md format uses YAML frontmatter to declare metadata and a markdown body for the skill content:
---
name: TypeScript Patterns
domain: development
triggers: [typescript, types, generics, inference, strict]
roles: [developer, lead]
confidence: low
---

## TypeScript Patterns

Prefer `unknown` over `any` for type-safe narrowing.
Use discriminated unions for state machines.
Confidence lifecycle:
LevelIconMeaning
low🔴First observation — pattern just noticed
medium🟡Confirmed across multiple sessions
high🟢Established team standard
Matching algorithm: SkillRegistry.matchSkills(task, role) awards +0.5 per trigger keyword found in the task description (capped at 0.7) and +0.3 if the agent’s role matches the skill’s roles list. Scores are clamped to [0, 1] and sorted descending.

Next steps

SDK Hooks & Tools

Full reference for HookPipeline, pre-tool and post-tool hooks, and the governance hook API.

State Backends

How to configure custom StorageProvider implementations and switch between backends.

Observability

OpenTelemetry integration, the Aspire dashboard, and TelemetryCollector configuration.

.NET Package

Use Squad from C# and F# with the official .NET SDK package.

Build docs developers (and LLMs) love