Documentation Index
Fetch the complete documentation index at: https://docs.orcbot.buzzchat.site/llms.txt
Use this file to discover all available pages before exploring further.
The Decision Pipeline is OrcBot’s safety and quality control layer. It sits between the LLM’s raw output and skill execution, applying guardrails to prevent loops, duplicates, unsafe operations, and premature task termination.
Architecture
1. Parser Layer
Purpose: Extract structured decisions from messy LLM output.
Implementation: ParserLayer.ts (lines 14-539)
3-Tier Fallback Strategy
// Tier 1: Native tool calling (OpenAI/Google function_call response)
if (response.toolCalls.length > 0) {
return normalizeNativeToolResponse(response.content, response.toolCalls);
}
// Tier 2: Clean JSON with minimal extraction
const jsonMatch = rawResponse.match(/```json\s*([\s\S]*?)```/);
if (jsonMatch) {
return JSON.parse(jsonMatch[1]);
}
// Tier 3: Aggressive extraction with schema repair
const extracted = extractFieldsFromText(rawResponse, ['reasoning', 'tools', 'completed']);
const repaired = schemaRepair(extracted);
return repaired;
StandardResponse Schema
interface StandardResponse {
reasoning: string; // LLM's thought process
tools: Tool[]; // Tool calls to execute
completed: boolean; // Is the task done?
summary?: string; // Final summary (when completed=true)
verification?: string; // Self-verification notes
content?: string; // Optional text content
}
interface Tool {
name: string; // Skill name (e.g., "web_search")
metadata: Record<string, any>; // Tool arguments
}
Parser Examples
Clean JSON (Tier 2):
LLM Output:
{
"reasoning": "I'll search for weather in Paris",
"tools": [{"name": "web_search", "metadata": {"query": "Paris weather"}}],
"completed": false
}
Parsed Output (Tier 2):
{
"reasoning": "I'll search for weather in Paris",
"tools": [{"name": "web_search", "metadata": {"query": "Paris weather"}}],
"completed": false
}
Messy Output (Tier 3):
LLM Output:
Let me think about this. I need to search for the weather in Paris.
Reasoning: The user wants weather info, so I'll use web_search.
Tools:
- web_search with query="Paris weather"
Completed: false
Parsed Output:
{
"reasoning": "The user wants weather info, so I'll use web_search.",
"tools": [{"name": "web_search", "metadata": {"query": "Paris weather"}}],
"completed": false
}
2. Deduplication Guard
Purpose: Prevent repeated identical tool calls within the same action.
Implementation: DecisionPipeline.ts (lines 45-120)
Strategy
Tracks recently executed tools using a sliding window:
// Dedup key = toolName + serialized args
const dedupKey = `${tool.name}:${JSON.stringify(tool.metadata, sortKeys)}`;
const window = this.recentToolCalls.get(actionId) || [];
if (window.includes(dedupKey)) {
return {
blocked: true,
reason: `Duplicate tool call: ${tool.name} with identical args already executed in this action`,
code: 'DEDUP_BLOCK'
};
}
// Add to window (max 20 entries)
window.push(dedupKey);
this.recentToolCalls.set(actionId, window.slice(-20));
Exemptions
Sequential UI components are exempt from deduplication (they’re intentionally called multiple times in sequence):
const sequentialUISkills = [
'telegram_send_buttons',
'telegram_send_poll',
'telegram_edit_message',
'telegram_react'
];
if (sequentialUISkills.includes(tool.name)) {
// Build side-effect key instead (includes message content)
dedupKey = `${tool.name}:${metadata.chatId}:${metadata.message.slice(0, 100)}`;
}
Example Block
[PIPELINE] Step 3: Blocking duplicate tool call
Tool: web_search
Args: {"query": "Paris weather"}
Reason: Same query already executed in step 1
Audit Code: DEDUP_BLOCK
[SYSTEM INJECTION]
Pipeline notes: Your proposed tool call (web_search) was blocked because you already
executed it in a previous step with identical arguments. You have the results in
memory — use them instead of re-searching.
3. Loop Detection
Purpose: Prevent infinite cycles of the same tool or tool sequences.
Implementation: DecisionPipeline.ts (lines 121-180)
Patterns Detected
1. Same-tool loops (3+ in a row):
const lastThree = toolHistory.slice(-3);
if (lastThree.every(t => t === toolName)) {
return { blocked: true, code: 'LOOP_SAME_TOOL' };
}
Example:
Step 1: web_search
Step 2: web_search
Step 3: web_search ← BLOCKED
2. Alternating tool loops (A→B→A→B):
const lastFour = toolHistory.slice(-4);
if (lastFour.length === 4 &&
lastFour[0] === lastFour[2] &&
lastFour[1] === lastFour[3]) {
return { blocked: true, code: 'LOOP_ALTERNATING' };
}
Example:
Step 1: web_search
Step 2: browser_navigate
Step 3: web_search
Step 4: browser_navigate ← BLOCKED
3. Search thrashing (multiple failed searches):
const recentSearches = toolHistory.slice(-5).filter(t =>
t === 'web_search' || t === 'browser_navigate'
);
if (recentSearches.length >= 4) {
const failedCount = recentErrors.filter(e =>
e.tool === 'web_search' || e.tool === 'browser_navigate'
).length;
if (failedCount >= 2) {
return { blocked: true, code: 'SEARCH_THRASHING' };
}
}
Example:
Step 1: web_search → FAILED (API error)
Step 2: browser_navigate → FAILED (timeout)
Step 3: web_search → OK
Step 4: web_search ← BLOCKED (thrashing detected)
Loop Recovery Suggestions
When a loop is detected, the pipeline injects recovery guidance:
const suggestions = {
LOOP_SAME_TOOL: "Try a different approach or tool. If web_search isn't working, use browser_navigate or deep_reason.",
LOOP_ALTERNATING: "You're stuck in a cycle. Step back and ask: what's the core problem? Consider using deep_reason to plan a new strategy.",
SEARCH_THRASHING: "Multiple searches have failed. The information may not be available online. Consider: (1) inform the user, (2) use a different data source, or (3) provide a partial answer based on what you know."
};
4. Channel Policy Guard
Purpose: Enforce cross-channel isolation and autonomy delivery rules.
Implementation: Agent.ts (lines 878-906)
Rules
1. Cross-channel send blocking (non-admin):
if (!isAdmin &&
action.source === 'telegram' &&
toolName === 'send_whatsapp' &&
!exemptTools.includes(toolName)) {
return {
blocked: true,
reason: 'Cross-channel send blocked. Action source is telegram but tool targets whatsapp.',
code: 'CROSS_CHANNEL_BLOCK'
};
}
Exemptions: send_email (intentionally cross-channel for “email this” requests)
2. Channel configuration check:
if (toolName === 'send_telegram' && !this.telegram) {
return {
blocked: true,
reason: 'Telegram channel is not configured or disabled.',
code: 'CHANNEL_DISABLED'
};
}
3. Autonomy delivery policy:
const allowedChannels = config.get('autonomyAllowedChannels'); // ['telegram']
const isHeartbeat = action.payload.isHeartbeat;
if (isHeartbeat &&
toolName === 'send_discord' &&
!allowedChannels.includes('discord')) {
return {
blocked: true,
reason: 'Autonomous sends to Discord are disabled by config (autonomyAllowedChannels).',
code: 'AUTONOMY_POLICY_BLOCK'
};
}
Example Block
[PIPELINE] Step 2: Blocking cross-channel send
Tool: send_whatsapp
Action Source: telegram
User: non-admin
Reason: Non-admin tasks cannot send to other channels
Audit Code: CROSS_CHANNEL_BLOCK
[SYSTEM INJECTION]
Your proposed tool (send_whatsapp) was blocked because this action originated from
Telegram and you're trying to send to WhatsApp. Use send_telegram instead to reply
to the user who made this request.
5. Safety Checks
Purpose: Prevent dangerous operations in safe mode and validate tool arguments.
Implementation: DecisionPipeline.ts (lines 181-250)
Safe Mode Blocks
if (config.get('safeMode')) {
const dangerousSkills = [
'run_command',
'write_file', 'create_file', 'delete_file',
'install_npm_dependency',
'browser_navigate', 'browser_click', 'browser_type'
];
if (dangerousSkills.includes(toolName)) {
return {
blocked: true,
reason: 'Safe mode is enabled. This tool is disabled for security.',
code: 'SAFE_MODE_BLOCK'
};
}
}
Argument Validation
// Missing required arguments
if (toolName === 'web_search' && !metadata.query) {
return {
blocked: true,
reason: 'Missing required argument: query',
code: 'INVALID_ARGS'
};
}
// Invalid argument types
if (toolName === 'send_telegram' && typeof metadata.chatId !== 'string') {
return {
blocked: true,
reason: 'Invalid argument: chatId must be a string',
code: 'INVALID_ARGS'
};
}
Restricted Path Access
const restrictedPaths = ['node_modules', '.git'];
if ((toolName === 'read_file' || toolName === 'write_file') &&
restrictedPaths.some(p => metadata.path.includes(p))) {
return {
blocked: true,
reason: 'Access to restricted paths (node_modules, .git) is blocked',
code: 'RESTRICTED_PATH'
};
}
6. Termination Review
Purpose: Prevent premature task completion before delivering substantive results.
Implementation: BlockReviewer.ts (lines 52-797)
Review Trigger
Runs when LLM sets completed: true:
if (decision.completed) {
const review = await blockReviewer.review(action, memory);
if (review.verdict === 'BLOCK') {
// Inject feedback and continue loop
memory.saveMemory({
id: `${actionId}-step-${step}-completion-audit-blocked`,
type: 'short',
content: `[SYSTEM: Completion blocked] ${review.reason}`,
metadata: {
actionId,
step,
auditCode: review.codes // e.g., ['ACK_ONLY', 'UNSENT_RESULTS']
}
});
decision.completed = false; // Force continuation
}
}
Audit Codes
| Code | Meaning | Example | Fix |
|---|
NO_SEND | No user-visible reply sent for a channel task | User asks question, agent searches but never sends answer | Use send_telegram before completing |
UNSENT_RESULTS | Deep tool output (search/browse/command) exists after last message | Agent runs web_search but completes without sharing results | Send summary: “Here’s what I found: [results]“ |
NO_SUBSTANTIVE | Only acknowledgements sent, no actual content | Agent says “Working on it…” then completes | Replace with concrete findings |
ACK_ONLY | Only status updates, no deliverable output | ”I’m searching…” → completes | Send: “Found: [actual results]“ |
ERROR_UNRESOLVED | Tool errors without explanation or recovery | web_search fails, agent completes silently | Explain: “Search failed due to [reason]” or retry |
GENERIC | Fallback for uncategorized issues | — | Review action memories manually |
Audit Logic
function auditCompletion(action, memory): ReviewResult {
const messages = memory.getActionMemories(actionId)
.filter(m => m.metadata?.role === 'assistant');
const lastMessage = messages[messages.length - 1];
const deepToolsUsed = memory.getActionMemories(actionId)
.filter(m => ['web_search', 'browser_navigate', 'run_command'].includes(m.metadata?.skill));
// 1. Check if any message was sent
if (action.source !== 'autonomy' && messages.length === 0) {
return {
verdict: 'BLOCK',
reason: 'No user-visible message sent for a channel task.',
codes: ['NO_SEND']
};
}
// 2. Check for unsent deep results
const lastDeepTool = deepToolsUsed[deepToolsUsed.length - 1];
if (lastDeepTool && lastDeepTool.timestamp > lastMessage?.timestamp) {
return {
verdict: 'BLOCK',
reason: 'You ran a research/command tool but did not send the results to the user.',
codes: ['UNSENT_RESULTS']
};
}
// 3. Check message quality
const lastContent = lastMessage?.content?.toLowerCase() || '';
const isAckOnly = /working on|searching|looking|checking|found it|done|okay|got it/i.test(lastContent) &&
lastContent.length < 100;
if (isAckOnly) {
return {
verdict: 'BLOCK',
reason: 'Your last message was just a status update. Share the actual findings.',
codes: ['ACK_ONLY']
};
}
// 4. Check for unresolved errors
const errors = memory.getActionMemories(actionId)
.filter(m => m.content.includes('FAILED') || m.content.includes('ERROR'));
if (errors.length > 0 && !lastContent.includes('error') && !lastContent.includes('failed')) {
return {
verdict: 'BLOCK',
reason: 'Tool errors occurred but you did not explain them to the user.',
codes: ['ERROR_UNRESOLVED']
};
}
// All checks passed
return { verdict: 'ALLOW' };
}
Example Block
User: "Find the weather in Paris"
Step 1: web_search("Paris weather") → SUCCESS (returned 10 results)
Step 2: LLM proposes {completed: true, summary: "Found weather info"}
[TERMINATION REVIEW]
Verdict: BLOCK
Reason: You ran web_search but never sent the results to the user.
Audit Codes: ['UNSENT_RESULTS']
[SYSTEM INJECTION]
[SYSTEM: Completion blocked] You ran web_search but never sent the results to the
user. The user cannot see your tool observations — they only see messages you send
them. Use send_telegram to deliver a summary of the weather before completing.
[CONTINUATION]
Step 3: LLM now sends: "Weather in Paris: 8°C, partly cloudy"
Step 4: LLM completes successfully
Prompt Helpers (PromptRouter)
Purpose: Modular, task-aware prompt assembly that activates only relevant guidance.
Implementation: PromptRouter.ts (lines 45-797) + src/core/prompts/ (8 helpers)
Active Helpers
The PromptRouter analyzes the task and selectively activates helpers:
const helpers = [
new DeliveryHelper(), // Always active (core formatting)
new ClarityHelper(), // Always active (output structure)
new ChannelHelper(), // Active when task mentions channels
new BrowserHelper(), // Active when task involves web/browser
new ResearchHelper(), // Active for research/search tasks
new CodeHelper(), // Active for coding/debugging tasks
new FileHelper(), // Active for file operations
new TerminationHelper() // Always active (completion guidance)
];
const activeHelpers = await router.route(taskDescription, metadata);
// Example: "Search for news" → [Delivery, Clarity, Research, Termination]
Token Savings
By only including relevant helpers, the router saves ~2,000-4,000 tokens per step:
Simple task ("ping"): 3 helpers → ~1,500 tokens
Complex task ("Build a web scraper"): 8 helpers → ~4,500 tokens
Vs. naive approach (all 8 helpers every time): ~4,500 tokens
Savings: ~3,000 tokens for simple tasks (60% reduction)
Helper Example: ResearchHelper
class ResearchHelper implements PromptHelper {
id = 'research';
async shouldActivate(context: PromptHelperContext): Promise<boolean> {
const keywords = ['search', 'find', 'research', 'look up', 'discover'];
return keywords.some(k => context.taskDescription.toLowerCase().includes(k));
}
async getInstructions(context: PromptHelperContext): string {
return `
## Research Protocol
1. Start with web_search for broad coverage
2. Use browser_navigate for deep dives on specific URLs
3. Extract structured data with extract_article
4. Synthesize findings before delivering
5. Cite sources when possible
Avoid:
- Searching the same query multiple times
- Browsing without reading the search results first
- Completing without delivering substantive findings
`;
}
}
Autopilot Mode
Purpose: Suppress clarification requests for fully autonomous operation.
Configuration:
autopilotNoQuestions: true
Effect:
if (config.get('autopilotNoQuestions')) {
// Block request_supporting_data skill
if (toolName === 'request_supporting_data') {
return {
blocked: true,
reason: 'Autopilot mode enabled — make best-effort decisions without asking',
code: 'AUTOPILOT_BLOCK'
};
}
// Inject instruction
systemPrompt += `\n\nAUTOPILOT MODE: Do not ask clarifying questions. Make reasonable assumptions and proceed.`;
}
Skill Routing Rules
Purpose: Intent-based skill selection for better tool matching.
Configuration:
skillRoutingRules:
- intent: search
preferSkills: [web_search, browser_navigate]
- intent: code
preferSkills: [run_command, read_file, write_file]
- intent: communication
preferSkills: [send_telegram, send_whatsapp, send_discord]
Effect:
When the task matches an intent, the preferred skills are highlighted in the prompt:
AVAILABLE SKILLS:
★ web_search(query) — Search the web [PREFERRED for this task]
★ browser_navigate(url) — Visit a URL [PREFERRED for this task]
http_fetch(url) — Lightweight HTTP GET/POST
... (40+ more skills)
Purpose: Prevent cross-user information leakage in multi-tenant deployments.
Rules:
// Non-admin tasks don't see elevated context
if (!isAdmin) {
skipJournal = true; // JOURNAL.md is admin-only
skipLearning = true; // LEARNING.md is admin-only
skipEpisodic = true; // Episodic summaries are admin-only
}
// Non-admin tasks can't execute elevated skills
const elevatedSkills = [
'run_command', 'write_file', 'manage_config', 'install_npm_dependency'
];
if (!isAdmin && elevatedSkills.includes(toolName)) {
return {
blocked: true,
reason: 'This skill requires admin privileges',
code: 'ELEVATED_SKILL_BLOCK'
};
}
Effect: Non-admin tasks only see their own short-term memory + thread context. No cross-user data leakage.
Pipeline overhead per step:
- Parser: ~10-50ms
- Deduplication: ~1-5ms
- Loop detection: ~2-10ms
- Channel policy: ~1-5ms
- Safety checks: ~2-10ms
- Termination review: ~500-2,000ms (LLM call)
- Total: ~520-2,080ms per step
Token costs:
- System prompt helpers: ~2,500-4,500 tokens (depends on task)
- Feedback injections: ~100-300 tokens per block
- Termination review prompt: ~500-1,000 tokens
Debugging Pipeline Blocks
1. Enable pipeline logs:
2. Grep for blocks:
grep "Pipeline blocked" ~/.orcbot/daemon.log
grep "AUDIT_BLOCK" ~/.orcbot/daemon.log
3. Inspect action memories:
const memories = memory.getActionMemories(actionId);
const blocks = memories.filter(m => m.content.includes('[SYSTEM: ') || m.content.includes('Pipeline notes:'));
console.log(blocks);
4. Test termination review:
const review = await blockReviewer.review(action, memory);
console.log(review);
// { verdict: 'BLOCK', reason: '...', codes: ['NO_SEND'] }
Configuration Reference
# Pipeline behavior
autopilotNoQuestions: false # Suppress clarification requests
overrideMode: false # Bypass safety checks (dangerous)
# Step limits
maxStepsPerAction: 15 # Max reasoning steps
maxMessagesPerAction: 10 # Max messages sent per action
actionTimeoutMs: 1800000 # 30 minutes
# Deduplication
messageDedupWindow: 300000 # 5 minutes
# Termination review
skipTerminationReview: false # Disable review (not recommended)
# Skill routing
skillRoutingRules:
- intent: search
preferSkills: [web_search]
# Safety
safeMode: false # Disable dangerous skills
pluginAllowList: [] # Empty = all allowed
pluginDenyList: [] # Block specific plugins
# Admin permissions
adminUserIds: [] # User IDs with elevated access
# Autonomy delivery
autonomyAllowedChannels: [] # Channels for proactive messages
Further Reading