agentleFS
ownpilotownpilot/OwnPilot/docs/AGENTS.md

OwnPilot

AGENTS.md426 starsChanged 3 months agoMITView on GitHub

This document provides a comprehensive reference for the OwnPilot agent system. It covers every layer of the architecture, from the core abstractions and execution loop through prompt composition, memory injection, permissions, provider routing, and the gateway integration that ties everything together. The OwnPilot agent system is a modular, multi-provider AI framework that supports tool-calling loops, dynamic prompt composition, persistent memory injection, fine-grained permissions, and multi-agent coordination. It is organized into three packages: The system provides two parallel agent abstractions: Both…

We split this AGENTS.md into the rules it gives an agent

22 rules. About 1 is common, 11 are distinctive, and 10 are very specific to this project.

  • ≈1

    Common · estimate

    Many files like this have a rule of this kind.

  • ≈11

    Distinctive · estimate

    This author's own choices. Worth a look.

  • 10

    Very specific

    Tied to their project's names, files or tools. Probably skip.

  • Common
  • Distinctive
  • Very specific
  1. Distinctive:

    Look up the team (by name or default).

    line 255
  2. Very specific:

    Call the team's router function with the user message and merged context.

    line 256
  3. Very specific:

    When enableDynamicPrompts is true and userId is provided, the orchestrator uses injectMemoryIntoPrompt to enhance the system prompt with …

    line 304
  4. Very specific:

    Truncation: If the composed prompt exceeds maxPromptLength, sections are dropped from the end while always preserving the base prompt.

    line 470
  5. Distinctive:

    Look up tool configuration in the policy.

    line 691
  6. Distinctive:

    If tool is unknown, deny access.

    line 692
  7. Very specific:

    If tool is in deniedTools, deny.

    line 694
  8. Very specific:

    If tool is in allowedTools, skip level and category checks.

    line 695
  9. Distinctive:

    Check permission level: user level must be >= tool required level.

    line 696
  10. Distinctive:

    Check category: tool category must be in user's allowed categories.

    line 697
  11. Distinctive:

    Check path restrictions (for file tools): file path must start with an allowed path.

    line 698
  12. Distinctive:

    Check host restrictions (for network tools): hostname must match allowed hosts.

    line 699
  13. Distinctive:

    Check rate limit: per-user, per-tool, 60-second sliding window.

    line 700
  14. Distinctive:

    Every AI provider implements:

    line 753
  15. Very specific:

    All providers inherit from BaseProvider which provides:

    line 779
  16. Distinctive:

    All providers use exponential backoff with jitter:

    line 789
  17. Very specific:

    Always fallback on TimeoutError.

    line 817
  18. Common:

    Do NOT fallback on API key errors or validation errors (they would fail everywhere).

    line 819
  19. Very specific:

    Default fallback order: anthropic -> openai -> google -> deepseek -> groq

    line 850
  20. Distinctive:

    When no LLM provider is configured, the generator falls back to built-in code templates for common patterns (Fibonacci, factorial, prime …

    line 898
  21. Very specific:

    Error recovery: When use_tool fails, the error response includes full parameter documentation so the LLM can fix its call and retry without …

    line 942
  22. Very specific:

    Default agents are loaded from data/seeds/default-agents.json via the seed loader at packages/gateway/src/db/seeds/default-agents.ts.

    line 1152

≈ “Common” is an estimate: at least 1 in 5 of the 36,368 AGENTS.md files we read have a rule of the same kind, not the same words. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.

Keep what fits and drop the rest. Your version, ready for your agents.
Files
AGENTS.md

Agent System Architecture

This document provides a comprehensive reference for the OwnPilot agent system. It covers every layer of the architecture, from the core abstractions and execution loop through prompt composition, memory injection, permissions, provider routing, and the gateway integration that ties everything together.


Table of Contents

  1. Overview
  2. Core Classes
    • Agent
    • AgentOrchestrator
    • AgentBuilder
    • MultiAgentOrchestrator
  3. Agent Configuration
    • AgentConfig (Agent class)
    • AgentConfig (Orchestrator)
  4. Execution Lifecycle
    • Agent Tool-Calling Loop
    • Orchestrator Execution Loop
    • Streaming Execution
    • Cancellation
  5. Prompt Composition
    • PromptComposer
    • Prompt Sections
    • Autonomy Guidelines
  6. Memory System
    • ConversationMemory
    • MemoryInjector
    • Memory Injection Flow
  7. Permission System
    • Permission Levels and Categories
    • PermissionChecker
    • Rate Limiting
  8. Provider System
    • IProvider Interface
    • Built-in Providers
    • FallbackProvider
    • ProviderRouter
  9. Code Generator
  10. Tool System
    • Tool Definition
    • Tool Registry
    • Meta-Tools
    • Tool Categories
  11. Planning System
  12. Gateway Integration
    • Database Schema
    • REST API
    • Agent Lifecycle in the Gateway
    • Default Agents
  13. Type Reference
  14. File Map

Overview

The OwnPilot agent system is a modular, multi-provider AI framework that supports tool-calling loops, dynamic prompt composition, persistent memory injection, fine-grained permissions, and multi-agent coordination. It is organized into three packages:

PackageRole
packages/coreAll agent abstractions, providers, tools, memory, permissions, prompt composition
packages/gatewayHTTP API (Hono), database persistence, tool executor wiring, agent lifecycle management
packages/uiReact frontend for agent configuration and chat

The system provides two parallel agent abstractions:

  1. Agent class (agent.ts) -- the production runtime used by the gateway. It manages conversation memory, a tool registry, streaming, and a turn-based tool-calling loop with approval callbacks.
  2. AgentOrchestrator class (orchestrator.ts) -- an event-driven orchestrator with an iteration-based loop, streaming generator, and memory injection support. It also provides the AgentBuilder fluent API and the MultiAgentOrchestrator for team-based agent coordination.

Both abstractions follow the same fundamental pattern: send messages to an LLM, check if the response contains tool calls, execute the tools, feed the results back, and repeat until the LLM produces a final text response.


Core Classes

Agent

File: packages/core/src/agent/agent.ts

The Agent class is the primary runtime abstraction used in production. It owns a provider, a tool registry, and a conversation memory instance.

class Agent {
  readonly name: string;

  constructor(
    config: AgentConfig,
    options?: {
      tools?: ToolRegistry;
      memory?: ConversationMemory;
    }
  );

  // State
  isReady(): boolean;
  getState(): Readonly<AgentState>;
  getConversation(): Conversation;

  // Tools
  getTools(): readonly ToolDefinition[];
  getAllToolDefinitions(): readonly ToolDefinition[];
  setAdditionalTools(toolNames: string[]): void;
  clearAdditionalTools(): void;
  getToolRegistry(): ToolRegistry;

  // Chat
  chat(message: string, options?: ChatOptions): Promise<Result<CompletionResponse, Error>>;

  // Memory
  getMemory(): ConversationMemory;
  reset(): Conversation;
  loadConversation(conversationId: string): boolean;
  fork(): Conversation | undefined;
  updateSystemPrompt(prompt: string): void;

  // Workspace
  setWorkspaceDir(dir: string | undefined): void;

  // Lifecycle
  cancel(): void;
}

Key behaviors:

  • On construction, core tools are registered unless a custom registry is provided.
  • A new conversation is created in memory with the configured system prompt.
  • The chat() method adds the user message to memory, then enters the tool-calling loop (processConversation).
  • Tool calls are filtered through an optional onBeforeToolCall approval callback before execution.
  • Approved tools run in parallel via Promise.all.
  • Callbacks onToolStart, onToolEnd, and onProgress provide real-time observability.
  • Supports both streaming and non-streaming modes.

Default limits:

ParameterDefault
maxTurns50
maxToolCalls200

AgentOrchestrator

File: packages/core/src/agent/orchestrator.ts

An event-driven orchestrator that manages an execution context (OrchestratorContext) through an iteration loop.

class AgentOrchestrator extends EventEmitter {
  constructor(config: AgentConfig);

  // Configuration
  getConfig(): AgentConfig;
  updateConfig(updates: Partial<AgentConfig>): void;
  setToolRegistry(registry: ToolRegistry): void;

  // Execution
  execute(
    userMessage: string,
    history?: Message[],
    metadata?: Record<string, unknown>
  ): Promise<OrchestratorContext>;
  stream(
    userMessage: string,
    history?: Message[],
    metadata?: Record<string, unknown>
  ): AsyncGenerator<AgentStep, OrchestratorContext>;

  // Control
  cancel(): void;
  getCurrentExecution(): OrchestratorContext | null;
}

Events emitted:

EventPayloadWhen
step(step: AgentStep, context)Each processing step (thinking, tool_call, tool_result, response)
tool_call(record: ToolCallRecord, context)After each tool execution completes
iteration(iteration: number, context)At the start of each loop iteration
complete(context)When execution finishes successfully
error(error: Error, context)When execution fails

Default parameters:

ParameterDefault
maxIterations10
maxTokens4096
temperature0.7
verbosefalse

AgentBuilder

File: packages/core/src/agent/orchestrator.ts (line 609)

A fluent builder API for constructing AgentOrchestrator instances.

import { createAgent } from './orchestrator.js';

const agent = createAgent()
  .name('Research Agent')
  .description('Performs web research')
  .systemPrompt('You are a research agent...')
  .provider(myProvider)
  .model('gpt-4o')
  .tool(searchDefinition, searchExecutor)
  .maxIterations(20)
  .maxTokens(8192)
  .temperature(0.5)
  .verbose()
  .build();

The build() method validates that name, systemPrompt, provider, and model are all set, then returns a configured AgentOrchestrator.


MultiAgentOrchestrator

File: packages/core/src/agent/orchestrator.ts (line 752)

Coordinates multiple agents organized into teams. Each team has a router function that selects the appropriate agent for a given message.

class MultiAgentOrchestrator extends EventEmitter {
  registerTeam(team: AgentTeam): void;
  setDefaultTeam(teamName: string): void;
  execute(
    message: string,
    teamName?: string,
    context?: Record<string, unknown>
  ): Promise<OrchestratorContext>;
  getTeams(): string[];
  getAgents(teamName: string): string[];
}

interface AgentTeam {
  name: string;
  agents: Map<string, AgentOrchestrator>;
  router: (message: string, context: Record<string, unknown>) => string;
  sharedContext: Record<string, unknown>;
}

Execution flow:

  1. Look up the team (by name or default).
  2. Call the team's router function with the user message and merged context.
  3. The router returns the name of the agent that should handle the request.
  4. That agent's execute() is called with the message and the team's shared context.

Agent Configuration

AgentConfig (Agent class)

Defined in packages/core/src/agent/types.ts:

interface AgentConfig {
  name: string;
  systemPrompt: string;
  provider: ProviderConfig;
  model: ModelConfig;
  tools?: readonly ToolId[]; // Filter: only expose these tools to the LLM
  maxTurns?: number; // Default: 50
  maxToolCalls?: number; // Default: 200
  memory?: MemoryConfig;
}

AgentConfig (Orchestrator)

Defined in packages/core/src/agent/orchestrator.ts:

interface AgentConfig {
  name: string;
  description?: string;
  systemPrompt: string;
  provider: LLMProvider;
  model: string;
  tools: ToolDefinition[];
  toolExecutors: Map<string, ToolExecutor>;
  maxIterations?: number; // Default: 10
  maxTokens?: number; // Default: 4096
  temperature?: number; // Default: 0.7
  verbose?: boolean;
  userId?: string;
  memoryOptions?: Omit<MemoryInjectionOptions, 'userId' | 'tools'>;
  enableDynamicPrompts?: boolean;
}

When enableDynamicPrompts is true and userId is provided, the orchestrator uses injectMemoryIntoPrompt to enhance the system prompt with user profile data, custom instructions, tool descriptions, and time context before each execution.


Execution Lifecycle

Agent Tool-Calling Loop

The Agent.processConversation() method implements the following loop:

1. Initialize turn counter = 0

2. WHILE turn < maxTurns:
   a. Increment turn counter
   b. Get full conversation context from memory (system prompt + messages)
   c. Build completion request with messages, model config, and tool definitions
   d. Fire onProgress callback

   e. IF streaming mode:
        Stream response, accumulate content and tool calls
      ELSE:
        Call provider.complete() synchronously

   f. Add assistant message to memory (content + tool calls)

   g. IF response contains tool calls:
        i.   Check total tool call count against maxToolCalls
        ii.  For each tool call, invoke onBeforeToolCall for approval
        iii. Separate approved and rejected tool calls
        iv.  Execute approved tools in PARALLEL (Promise.all)
             - Fire onToolStart before each
             - Fire onToolEnd after each (with duration)
        v.   Combine approved results + rejection messages
        vi.  Add tool results to memory
        vii. Update tool call counter
        viii. CONTINUE loop (go back to step 2)

   h. IF response has NO tool calls:
        Return final response (exit loop)

3. IF loop exhausted: return error "Maximum turns exceeded"

Important implementation details:

  • Tool calls are detected by checking response.toolCalls array presence regardless of finishReason. Some providers (notably Google) return stop even when tool calls are present.
  • Rejected tool calls produce an error result message that is fed back to the LLM so it can adapt.
  • The isProcessing state flag prevents concurrent chat() calls on the same agent.

Orchestrator Execution Loop

The AgentOrchestrator.runExecutionLoop() follows a similar pattern but operates on a mutable OrchestratorContext:

1. Create execution context with unique ID, status = 'running'
2. Compose system prompt (with optional memory injection)
3. Build initial messages: [system, ...history, user]

4. WHILE iteration < maxIterations AND not aborted:
   a. Increment iteration counter
   b. Emit 'iteration' event
   c. Call provider.complete() with current messages

   d. IF response has tool calls:
        i.   Convert OpenAI-format tool calls to internal format
        ii.  Add assistant message with tool calls
        iii. Execute each tool sequentially via executeToolCall()
        iv.  Add tool result messages
        v.   Record ToolCallRecord with timing and success status
        vi.  Emit 'tool_call' event for each
        vii. CONTINUE loop

   e. IF no tool calls:
        i.   Add assistant message
        ii.  Set context.response
        iii. BREAK loop

5. If max iterations hit without response, extract last content or '[Max iterations reached]'
6. Set context.status = 'completed' or 'failed'
7. Emit 'complete' or 'error' event

Key difference from the Agent class: The orchestrator executes tools sequentially rather than in parallel, and it uses an event-emitter pattern instead of callbacks.

Streaming Execution

Both abstractions support streaming:

Agent streaming:

  • The chat() method accepts stream: true and an onChunk callback.
  • Tool calls are accumulated across stream chunks by matching tool call IDs.
  • Streaming and tool-calling are fully interleaved: the agent streams the LLM response, processes any tool calls, then streams the next iteration.

Orchestrator streaming:

  • The stream() method returns an AsyncGenerator<AgentStep>.
  • Yields AgentStep objects with types: thinking, tool_call, tool_result, response.
  • Falls back to non-streaming complete() if the provider does not implement stream().

Cancellation

Both abstractions support cancellation via AbortController:

  • Agent: agent.cancel() aborts the provider's in-flight request and resets isProcessing.
  • Orchestrator: orchestrator.cancel() aborts the controller and sets context status to cancelled. The execution loop checks abortController.signal.aborted at each iteration boundary.

Prompt Composition

PromptComposer

File: packages/core/src/agent/prompt-composer.ts

The PromptComposer dynamically assembles a system prompt from multiple contextual sections. It operates on a PromptContext object and produces a single string by joining sections with \n\n---\n\n dividers.

class PromptComposer {
  constructor(options?: PromptComposerOptions);
  compose(context: PromptContext): string;
}

PromptComposerOptions:

OptionTypeDefaultDescription
includeToolDescriptionsbooleantrueInclude tool availability section
includeUserProfilebooleantrueInclude user facts, preferences, interests
includeTimeContextbooleantrueInclude current date/time and timezone
includeCapabilitiesbooleantrueInclude agent capability list
maxPromptLengthnumber16000Maximum characters before truncation

PromptContext fields:

FieldTypeDescription
basePromptstringThe agent's core system prompt
userProfileUserProfileName, facts, preferences, communication style, interests, goals
toolsToolDefinition[]Available tools for the tool workflow section
customInstructionsstring[]User-defined instructions that must always be followed
timeContextTimeContextCurrent time, timezone, day of week, time of day
capabilitiesAgentCapabilitiesCode execution, file access, web, memory, scheduling, autonomy level
conversationContextPromptConversationContextMessage count, topics, current task, previous summary
workspaceContextWorkspaceContextWorkspace directory, home directory, temp directory

Prompt Sections

The composer assembles sections in this order:

  1. Base Prompt -- the agent's identity and core instructions.
  2. User Profile -- personal facts, communication style, interests, goals.
  3. Custom Instructions -- user-defined rules the agent must always follow.
  4. Available Tools -- tool category summary and the mandatory 3-step tool workflow:
    • Step 1: search_tools("keyword") to discover tools.
    • Step 2: get_tool_help("tool_name") to learn parameters.
    • Step 3: use_tool("tool_name", { params }) to execute.
  5. Automation System -- trigger and plan documentation (only if automation tools are registered).
  6. Workspace Context -- allowed directories for file operations.
  7. Capabilities -- what the agent can do (code execution, file access, web browsing, memory, scheduling).
  8. Autonomy Guidelines -- behavioral rules based on autonomy level.
  9. Time Context -- current date, day, timezone.
  10. Conversation Context -- message count, topics, current task.

Truncation: If the composed prompt exceeds maxPromptLength, sections are dropped from the end while always preserving the base prompt.

Autonomy Guidelines

The autonomy level controls how aggressively the agent acts without user confirmation:

LevelBehavior
noneAsk for explicit permission before taking any action
lowRead-only operations are free; ask for any modifications
mediumMost operations are free; ask for destructive or irreversible actions
highAlmost all operations are autonomous; ask only for truly destructive actions
fullFull autonomy; take action immediately

The autonomy level is sourced from the user's AI preferences in their personal memory profile.


Memory System

ConversationMemory

File: packages/core/src/agent/memory.ts

An in-memory conversation store that manages multiple conversations with configurable message and token limits.

class ConversationMemory {
  constructor(config?: MemoryConfig);

  // Conversation lifecycle
  create(systemPrompt?: string, metadata?: Record<string, unknown>): Conversation;
  get(id: string): Conversation | undefined;
  has(id: string): boolean;
  delete(conversationId: string): boolean;
  fork(conversationId: string): Conversation | undefined;
  clear(): void;

  // Messages
  addMessage(conversationId: string, message: Message): Conversation | undefined;
  addUserMessage(conversationId: string, content: string | ContentPart[]): Conversation | undefined;
  addAssistantMessage(
    conversationId: string,
    content: string,
    toolCalls?: ToolCall[]
  ): Conversation | undefined;
  addToolResults(conversationId: string, results: ToolResult[]): Conversation | undefined;
  clearMessages(conversationId: string): boolean;

  // Context
  getContextMessages(conversationId: string): readonly Message[];
  getFullContext(conversationId: string): readonly Message[];

  // Management
  updateSystemPrompt(conversationId: string, systemPrompt: string): Conversation | undefined;
  updateMetadata(
    conversationId: string,
    metadata: Record<string, unknown>
  ): Conversation | undefined;
  getStats(
    conversationId: string
  ): { messageCount: number; estimatedTokens: number; lastActivity: Date } | undefined;
  export(conversationId: string): string | undefined;
  import(json: string): Conversation | undefined;
}

MemoryConfig:

OptionTypeDefaultDescription
maxMessagesnumber100Maximum messages to retain
maxTokensnumber100000Token budget for context window
summarizebooleanfalseWhether to summarize old messages
persistencestring'session''none', 'session', or 'persistent'

Token estimation: Characters are divided by 4 to approximate token count. Tool calls and tool results add their string length plus 50 characters overhead each.

Context windowing: getContextMessages() trims messages from the beginning (oldest first) to fit within the maxTokens budget. getFullContext() prepends the system prompt.

Forking: fork() creates a deep copy of a conversation with a new UUID and a forkedFrom metadata field.

MemoryInjector

File: packages/core/src/agent/memory-injector.ts

The MemoryInjector bridges the personal memory store and the prompt composer. It fetches the user's comprehensive profile, converts it to a UserProfile, extracts custom instructions, and delegates to PromptComposer.compose().

class MemoryInjector {
  injectMemory(basePrompt: string, options?: MemoryInjectionOptions): Promise<InjectedPromptResult>;
  createAgentPrompt(agentName: string, agentDescription: string, options?: ...): Promise<string>;
  getRelevantContext(userId: string, query: string): Promise<string | null>;
}

MemoryInjectionOptions:

OptionTypeDescription
userIdstringUser ID for personal memory retrieval
toolsToolDefinition[]Tools to include in prompt
capabilitiesAgentCapabilitiesAgent capability flags
conversationContextPromptConversationContextCurrent conversation metadata
workspaceContextWorkspaceContextFile operation directories
includeProfilebooleanWhether to include user profile (default: true)
includeInstructionsbooleanWhether to include custom instructions (default: true)
includeTimeContextbooleanWhether to include time context (default: true)
includeToolDescriptionsbooleanWhether to include tool section (default: true)
maxPromptLengthnumberMaximum prompt length

InjectedPromptResult:

FieldTypeDescription
systemPromptstringThe fully composed system prompt
userProfileUserProfileThe user profile used (if any)
toolCountnumberNumber of tools included
instructionCountnumberNumber of custom instructions included
hasTimeContextbooleanWhether time context was included
promptLengthnumberTotal character count of the prompt

Memory Injection Flow

The complete flow from user message to enriched prompt:

1. Gateway receives chat request
2. Gateway calls injectMemoryIntoPrompt(basePrompt, options)
3. MemoryInjector:
   a. Creates PromptContext with base prompt, tools, capabilities
   b. Adds TimeContext (current date, timezone, time of day)
   c. If userId provided:
      - Loads ComprehensiveProfile from PersonalMemoryStore
      - Converts to UserProfile format (identity, facts, communication style, interests, goals)
      - Extracts customInstructions from AI preferences
      - Applies autonomyLevel from profile to capabilities
   d. Delegates to PromptComposer.compose(context)
4. PromptComposer assembles sections:
   [Base Prompt]
   ---
   [User Profile]
   ---
   [Custom Instructions]
   ---
   [Available Tools + Workflow]
   ---
   [Automation System]
   ---
   [Workspace Context]
   ---
   [Capabilities]
   ---
   [Autonomy Guidelines]
   ---
   [Time Context]
   ---
   [Conversation Context]
5. Returns composed systemPrompt to gateway
6. Gateway creates/updates Agent with the enriched prompt

Additionally, the gateway builds separate memory and goal contexts by querying the MemoriesRepository and GoalsRepository databases. These are prepended to the base prompt before memory injection:

basePrompt + memoryContext + goalContext -> injectMemoryIntoPrompt() -> enrichedPrompt

Permission System

File: packages/core/src/agent/permissions.ts

Permission Levels and Categories

Permission hierarchy (ascending privilege):

none -> read -> write -> execute -> admin

Tool categories:

CategoryDescriptionExample tools
file_readRead files and directoriesread_file, list_directory, search_files, file_info
file_writeCreate or modify fileswrite_file, download_file, copy_file
file_deleteDelete files or directoriesdelete_file
code_executeRun code in sandboxexecute_javascript, execute_python, compile_code
network_readHTTP GET, fetch pageshttp_request, fetch_web_page, search_web, json_api
network_writeHTTP POST/PUT/DELETE(network write operations)
systemSystem commandsexecute_shell, package_manager
memoryMemory managementmemory_store, memory_recall
customPlugin and custom tools(user-defined tools)

PermissionChecker

class PermissionChecker {
  constructor(policy?: PermissionPolicy);

  check(
    toolName: string,
    context: ToolContext,
    args?: Record<string, unknown>
  ): PermissionCheckResult;
  recordUsage(
    toolName: string,
    context: ToolContext,
    result: PermissionCheckResult,
    args?: Record<string, unknown>
  ): void;
  getToolConfig(toolName: string): ToolPermissionConfig | undefined;
  updatePolicy(updates: Partial<PermissionPolicy>): PermissionPolicy;
  addUserPermissions(userId: string, permissions: Omit<UserPermissions, 'userId'>): void;
}

Permission check sequence:

  1. Look up tool configuration in the policy.
  2. If tool is unknown, deny access.
  3. Get user permissions (user-specific or default).
  4. If tool is in deniedTools, deny.
  5. If tool is in allowedTools, skip level and category checks.
  6. Check permission level: user level must be >= tool required level.
  7. Check category: tool category must be in user's allowed categories.
  8. Check path restrictions (for file tools): file path must start with an allowed path.
  9. Check host restrictions (for network tools): hostname must match allowed hosts.
  10. Check rate limit: per-user, per-tool, 60-second sliding window.
  11. Determine if confirmation is required.

PermissionCheckResult:

interface PermissionCheckResult {
  allowed: boolean;
  reason?: string; // Reason for denial
  requiresConfirmation?: boolean; // Whether user must confirm
  context?: Record<string, unknown>;
}

Rate Limiting

Rate limits are enforced per userId:toolName key with a 60-second sliding window. When a tool has a rateLimit configured, the checker tracks call counts and resets them after one minute. If the global globalRateLimit is set (default: 60 calls/minute), it applies to all tools.

Tool-specific rate limits from the default configuration:

ToolRate Limit (calls/min)
execute_javascript10
execute_python10
execute_shell5
compile_code5
package_manager3
search_web20

Factory functions:

// Default policy (read-only by default)
createPermissionChecker(policy?: Partial<PermissionPolicy>): PermissionChecker;

// Read-only (file_read + memory only)
createRestrictiveChecker(): PermissionChecker;

// Full access (all categories, admin level)
createPermissiveChecker(): PermissionChecker;

// Wrap a tool executor with permission checking
withPermissionCheck(toolName, executor, checker): WrappedExecutor;

Provider System

IProvider Interface

File: packages/core/src/agent/provider.ts

Every AI provider implements:

interface IProvider {
  readonly type: AIProvider;
  isReady(): boolean;
  complete(request: CompletionRequest): Promise<Result<CompletionResponse, Error>>;
  stream(request: CompletionRequest): AsyncGenerator<Result<StreamChunk, Error>>;
  countTokens(messages: readonly Message[]): number;
  getModels(): Promise<Result<string[], Error>>;
}

Supported AIProvider types:

openai, anthropic, google, deepseek, groq, mistral, zhipu, cohere, together, fireworks, perplexity, openrouter, xai, local, custom

Built-in Providers

ProviderClassBase URLAuth
OpenAIOpenAIProviderhttps://api.openai.com/v1Bearer token
AnthropicAnthropicProviderhttps://api.anthropic.com/v1x-api-key header
GoogleGoogleProviderNative Gemini SDKAPI key
OthersOpenAICompatibleProviderProvider-specificBearer token

All providers inherit from BaseProvider which provides:

  • cancel() via AbortController
  • createFetchOptions() with configurable timeout (default: 5 minutes)
  • parseToolCalls() for normalizing tool call formats
  • buildMessages() for converting internal messages to OpenAI format
  • buildTools() for converting tool definitions to OpenAI function calling format
  • countTokens() approximation (~4 chars per token)

Retry configuration: All providers use exponential backoff with jitter:

ParameterValue
Max retries3
Initial delay1000ms
Max delay10000ms
Backoff multiplier2
Jitterenabled

FallbackProvider

File: packages/core/src/agent/providers/fallback.ts

Wraps a primary provider with ordered fallback providers. On failure, it automatically tries the next provider.

class FallbackProvider implements IProvider {
  constructor(config: FallbackProviderConfig);
  complete(request): Promise<Result<CompletionResponse, Error>>;
  stream(request): AsyncGenerator<Result<StreamChunk, Error>>;
  getCurrentProvider(): AIProvider;
  cancel(): void;
}

Fallback rules:

  • Always fallback on TimeoutError.
  • Fallback on rate limits (429), server errors (500-504), network errors.
  • Do NOT fallback on API key errors or validation errors (they would fail everywhere).
  • Unknown errors: attempt fallback.

ProviderRouter

File: packages/core/src/agent/providers/router.ts

Smart routing between providers based on cost, speed, quality, and capability requirements.

class ProviderRouter {
  constructor(config?: RouterConfig);
  getAvailableProviders(): ResolvedProviderConfig[];
  selectProvider(criteria?, strategy?): Result<RoutingResult, Error>;
  complete(request, criteria?, strategy?): Promise<Result<CompletionResponse & { routingInfo }, Error>>;
  stream(request, criteria?, strategy?): AsyncGenerator<...>;
  completeWithFallback(request, criteria?): Promise<Result<... & { attempts: string[] }, Error>>;
  estimateCost(inputTokens, outputTokens, criteria?, strategy?): Result<{ estimatedCost }, Error>;
}

Routing strategies:

StrategyBehavior
cheapestSelect the lowest-cost model with required capabilities
fastestSelect the lowest-latency model
smartestSelect the highest-quality model for complex reasoning
balancedBalance cost and quality (default)
fallbackTry providers in priority order until one succeeds

Default fallback order: anthropic -> openai -> google -> deepseek -> groq

Provider instances are cached to avoid repeated initialization.


Code Generator

File: packages/core/src/agent/code-generator.ts

AI-driven code generation with sandbox execution support.

class CodeGenerator {
  constructor(config?: Partial<CodeGeneratorConfig>);
  setLLMProvider(provider: CodeLLMProvider): void;
  generate(request: CodeGenerationRequest): Promise<CodeGenerationResponse>;
  execute(code: string, language: CodeLanguage, options?): Promise<CodeExecutionResult>;
  validateCode(code: string, language: CodeLanguage): { valid: boolean; errors: string[] };
  isExecutable(language: CodeLanguage): boolean;
  saveSnippet(userId, snippet): CodeSnippet;
  getSnippets(userId, filter?): CodeSnippet[];
  getStats(userId?): ExecutionStats;
}

Supported languages: javascript, typescript, python, shell, sql, html, css, json, markdown

Executable languages (sandbox): javascript, typescript only

Code validation checks:

  • Maximum code length (default: 50,000 characters)
  • Language-specific validation (JavaScript/TypeScript)
  • Dangerous pattern detection:
    • process.exit
    • require('child_process')
    • require('fs')
    • eval()
    • Function()
    • Dynamic import()

Sandbox defaults:

  • Allowed: timers, crypto
  • Denied: network, env, spawn, fs read, fs write
  • Resource limits: 64MB memory, 30s CPU time, 30s execution timeout

When no LLM provider is configured, the generator falls back to built-in code templates for common patterns (Fibonacci, factorial, prime checks, sorting, date formatting, array utilities).


Tool System

Tool Definition

interface ToolDefinition {
  name: string; // Unique identifier (e.g., "read_file")
  description: string; // Human-readable description
  parameters: {
    type: 'object';
    properties: Record<string, JSONSchemaProperty>;
    required?: string[];
  };
  requiresConfirmation?: boolean; // Whether to ask the user first
  category?: string; // Grouping category
  tags?: string[]; // Hidden search tags for discovery
}

Tool Registry

The ToolRegistry manages tool definitions and their executors. It supports:

  • Registration and unregistration
  • Executor updates (for plugin overrides)
  • Filtering by names
  • Tool discovery by name or definition
  • Workspace directory configuration
  • API key center integration

Meta-Tools

The system exposes three meta-tools to the LLM instead of 100+ individual tool schemas. This reduces token consumption from approximately 20K+ tokens per request to under 1K.

Meta-ToolPurpose
search_tools(query, category?)Keyword search across all registered tools. Supports AND matching, category filtering, and "all" to list everything.
get_tool_help(tool_name)Returns detailed help for a specific tool: description, required/optional parameters with types, default values, examples, and rate limits.
use_tool(tool_name, arguments)Proxy executor that validates parameters, applies max limits, executes the tool, and auto-includes parameter help on errors for LLM self-correction.

Error recovery: When use_tool fails, the error response includes full parameter documentation so the LLM can fix its call and retry without needing a separate get_tool_help call.

Tool max limits: List-returning tools have enforced maximum values (e.g., list_emails capped at 50 results) via the applyToolLimits() function to prevent unbounded queries.

Tool Categories

The system organizes tools into these categories:

CategoryTools
Tasksadd_task, batch_add_tasks, list_tasks, complete_task, update_task, delete_task
Bookmarksadd_bookmark, batch_add_bookmarks, list_bookmarks, delete_bookmark
Notesadd_note, batch_add_notes, list_notes, update_note, delete_note
Calendaradd_calendar_event, batch_add_calendar_events, list_calendar_events, delete_calendar_event
Contactsadd_contact, batch_add_contacts, list_contacts, update_contact, delete_contact
Custom Datacreate_custom_table, add_custom_record, list_custom_records, search_custom_records, ...
File Systemread_file, write_file, list_directory, search_files, download_file, file_info, ...
PDFread_pdf, create_pdf, pdf_info
Code Executionexecute_javascript, execute_python, execute_shell, compile_code, package_manager
Gitgit_status, git_diff, git_log, git_commit, git_add, git_branch, git_checkout
Web and APIhttp_request, fetch_web_page, search_web, json_api
Emailsend_email, list_emails, read_email, delete_email, search_emails, reply_email
Imageanalyze_image, generate_image, edit_image, image_variation, resize_image
Audiotext_to_speech, speech_to_text, translate_audio, audio_info, split_audio
Translationtranslate_text, detect_language, list_languages, batch_translate
Data Extractionextract_structured_data, extract_entities, extract_table_data, summarize_text
Vector Searchcreate_embedding, semantic_search, upsert_vectors, delete_vectors, ...
Financeadd_expense, batch_add_expenses, parse_receipt, query_expenses, export_expenses, ...
Weatherget_weather, get_weather_forecast
Memoryremember, batch_remember, recall, forget, list_memories, boost_memory, memory_stats
Goalscreate_goal, list_goals, update_goal, decompose_goal, get_next_actions, ...
Dynamic Toolscreate_tool, list_custom_tools, delete_custom_tool, toggle_custom_tool
Utilitiesget_current_datetime, calculate, convert_units, generate_uuid, regex, format_json, ...

Planning System

File: packages/core/src/agent/orchestrator.ts (line 820)

The planning system decomposes complex goals into executable step sequences.

interface Plan {
  goal: string;
  steps: PlanStep[];
  status: 'pending' | 'in_progress' | 'completed' | 'failed';
  currentStep: number;
}

interface PlanStep {
  id: number;
  description: string;
  toolName?: string;
  toolArgs?: Record<string, unknown>;
  dependsOn: number[];
  status: 'pending' | 'in_progress' | 'completed' | 'skipped' | 'failed';
  result?: unknown;
}

createPlanningPrompt(goal, availableTools) generates a prompt that instructs the LLM to decompose a goal into numbered steps, specifying which tool to use for each and listing dependencies.

parsePlan(response) extracts the JSON plan from the LLM's response using regex matching.

Additionally, the gateway provides plan management tools:

  • create_plan(name, goal) -- create a new plan
  • add_plan_step(plan_id, order, type, name, ...) -- add steps (types: tool_call, llm_decision, user_input, condition, parallel, loop)
  • execute_plan(plan_id) -- execute with dependency resolution, retry with exponential backoff, deadlock detection
  • pause_plan, delete_plan, list_plans, get_plan_details

Gateway Integration

Database Schema

Agent records are stored in a database table:

CREATE TABLE agents (
  id TEXT PRIMARY KEY,
  name TEXT NOT NULL,
  system_prompt TEXT,
  provider TEXT NOT NULL,
  model TEXT NOT NULL,
  config TEXT NOT NULL DEFAULT '{}',  -- JSON blob
  created_at TIMESTAMP DEFAULT NOW(),
  updated_at TIMESTAMP DEFAULT NOW()
);

The config JSON blob contains:

{
  "maxTokens": 8192,
  "temperature": 0.7,
  "maxTurns": 25,
  "maxToolCalls": 200,
  "tools": ["read_file", "write_file"],
  "toolGroups": ["file_system", "memory"]
}

Repository: packages/gateway/src/db/repositories/agents.ts

class AgentsRepository extends BaseRepository {
  create(data): Promise<AgentRecord>;
  getById(id: string): Promise<AgentRecord | null>;
  getByName(name: string): Promise<AgentRecord | null>;
  getAll(): Promise<AgentRecord[]>;
  update(id: string, data): Promise<AgentRecord | null>;
  delete(id: string): Promise<boolean>;
  count(): Promise<number>;
}

REST API

Base path: /api/agents

MethodPathDescription
GET/List all agents
POST/Create a new agent
GET/:idGet agent details
PATCH/:idUpdate agent configuration
DELETE/:idDelete an agent
POST/:id/resetReset agent conversation
POST/resyncResync from default agents JSON

Create agent request:

{
  "name": "Research Agent",
  "systemPrompt": "You are a research assistant...",
  "provider": "default",
  "model": "default",
  "maxTokens": 8192,
  "temperature": 0.7,
  "maxTurns": 25,
  "maxToolCalls": 200,
  "tools": ["read_file"],
  "toolGroups": ["web_access", "memory"]
}

Provider and model default to "default", which resolves at runtime to the user's configured default provider and model.

Agent Lifecycle in the Gateway

The gateway manages the full lifecycle of runtime Agent instances:

1. CREATION (POST /api/agents)
   - Store configuration in database
   - Runtime Agent is NOT created yet (lazy initialization)

2. FIRST USE (chat request referencing the agent)
   a. Load AgentRecord from database
   b. Resolve "default" provider/model to actual values from user settings
   c. Load API key for the resolved provider
   d. Create ToolRegistry and register ALL tool categories:
      - Core tools (file, code, web, etc.)
      - Memory tools (with gateway executor wiring)
      - Goal tools (with gateway executor wiring)
      - Custom data tools
      - Personal data tools (tasks, bookmarks, notes, calendar, contacts)
      - Config center tools
      - Trigger management tools
      - Plan management tools
      - Dynamic tool meta-tools (search_tools, get_tool_help, use_tool)
      - Active custom tools (user-created)
      - Channel tools
      - Plugin tools (with core stub replacement)
   e. Remove superseded core stubs when plugins provide real implementations
   f. Build memory context from persistent memories database
   g. Build goal context from active goals database
   h. Compose enriched system prompt via injectMemoryIntoPrompt()
   i. Configure Agent with meta-tool filter (only expose search_tools, get_tool_help, use_tool)
   j. Create and cache Agent instance

3. CACHING
   - Named agents: cached in agentCache (Map<string, Agent>)
   - Chat agents: cached in chatAgentCache (Map<string, Agent>) keyed by "chat:{provider}:{model}"
   - Config cached in agentConfigCache (Map<string, AgentConfig>)

4. INVALIDATION
   - Agent update (PATCH): cache entry deleted, recreated on next use
   - Agent delete (DELETE): cache entry deleted
   - Plugin/tool changes: invalidateAgentCache() clears all caches
   - New chat: resetChatAgentContext() creates fresh conversation within existing agent

5. TOOL EXECUTION FLOW (at runtime)
   a. LLM calls search_tools("keyword") -> returns matching tool names
   b. LLM calls get_tool_help("tool_name") -> returns parameter schema and example
   c. LLM calls use_tool("tool_name", { args }) -> gateway validates and executes:
      - Pre-validates required parameters
      - Applies max limits for list-returning tools
      - Executes via ToolRegistry
      - On error: returns error + auto-generated parameter help for self-correction
   d. All tool executions are traced via the tracing system

Default Agents

Default agents are loaded from data/seeds/default-agents.json via the seed loader at packages/gateway/src/db/seeds/default-agents.ts.

Each default agent has:

interface AgentSeed {
  id: string;
  name: string;
  systemPrompt: string;
  provider: string; // Always "default"
  model: string; // Always "default"
  config: {
    maxTokens: number;
    temperature: number;
    maxTurns: number;
    maxToolCalls: number;
    tools?: string[];
    toolGroups?: string[];
  };
}

The JSON file supports additional fields: emoji, category, dataAccess, and triggers (with keyword matching for automatic agent selection).

The POST /api/agents/resync endpoint reloads defaults from the JSON file, updating existing agents and creating new ones.


Type Reference

Core Message Types

type MessageRole = 'system' | 'user' | 'assistant' | 'tool';

interface Message {
  role: MessageRole;
  content: string | ContentPart[];
  toolCalls?: ToolCall[];
  toolResults?: ToolResult[];
  metadata?: Record<string, unknown>;
}

interface ToolCall {
  id: string;
  name: string;
  arguments: string; // JSON string
  metadata?: Record<string, unknown>;
}

interface ToolResult {
  toolCallId: string;
  content: string;
  isError?: boolean;
}

Execution Context Types

interface OrchestratorContext {
  id: string;
  iteration: number;
  messages: Message[];
  toolCalls: ToolCallRecord[];
  status: 'running' | 'completed' | 'failed' | 'cancelled';
  startTime: Date;
  endTime?: Date;
  response?: string;
  error?: string;
  metadata: Record<string, unknown>;
}

interface ToolCallRecord {
  name: string;
  arguments: Record<string, unknown>;
  result: unknown;
  startTime: Date;
  endTime: Date;
  duration: number;
  success: boolean;
  error?: string;
}

interface AgentStep {
  type: 'thinking' | 'tool_call' | 'tool_result' | 'response';
  content: unknown;
  timestamp: Date;
}

interface AgentState {
  conversation: Conversation;
  toolCallCount: number;
  turnCount: number;
  isProcessing: boolean;
  lastError?: string;
}

Completion Types

interface CompletionRequest {
  messages: readonly Message[];
  model: ModelConfig;
  tools?: readonly ToolDefinition[];
  toolChoice?: 'auto' | 'none' | 'required' | { name: string };
  stream?: boolean;
  user?: string;
}

interface CompletionResponse {
  id: string;
  content: string;
  toolCalls?: readonly ToolCall[];
  finishReason: 'stop' | 'length' | 'tool_calls' | 'content_filter' | 'error';
  usage?: TokenUsage;
  model: string;
  createdAt: Date;
}

interface StreamChunk {
  id: string;
  content?: string;
  toolCalls?: readonly Partial<ToolCall>[];
  done: boolean;
  finishReason?: CompletionResponse['finishReason'];
  usage?: TokenUsage;
}

interface TokenUsage {
  promptTokens: number;
  completionTokens: number;
  totalTokens: number;
  cachedTokens?: number;
}

File Map

FileContents
packages/core/src/agent/agent.tsAgent class, createAgent(), createSimpleAgent()
packages/core/src/agent/orchestrator.tsAgentOrchestrator, AgentBuilder, MultiAgentOrchestrator, Plan, planning utilities
packages/core/src/agent/types.tsAll TypeScript interfaces and type definitions
packages/core/src/agent/provider.tsIProvider, BaseProvider, OpenAIProvider, AnthropicProvider, createProvider()
packages/core/src/agent/providers/google.tsGoogleProvider (native Gemini SDK)
packages/core/src/agent/providers/openai-compatible.tsOpenAICompatibleProvider for third-party providers
packages/core/src/agent/providers/fallback.tsFallbackProvider with automatic provider failover
packages/core/src/agent/providers/router.tsProviderRouter with strategy-based selection
packages/core/src/agent/providers/configs/Provider config loader and sync logic
data/providers/Provider JSON configuration files (100+), synced from models.dev
packages/core/src/events/EventBus -- typed event system with wildcard subscriptions for agent, tool, and system events
packages/core/src/agent/prompt-composer.tsPromptComposer, WorkspaceContext, TimeContext, AgentCapabilities
packages/core/src/agent/memory-injector.tsMemoryInjector, injectMemoryIntoPrompt(), createEnhancedAgentPrompt()
packages/core/src/agent/memory.tsConversationMemory, createMemory()
packages/core/src/agent/permissions.tsPermissionChecker, permission levels, categories, policies
packages/core/src/agent/code-generator.tsCodeGenerator, sandbox execution, code validation
packages/core/src/agent/tools.tsToolRegistry, registerCoreTools()
packages/core/src/agent/tools/index.tsAll tool exports, registerAllTools(), TOOL_CATEGORIES, TOOL_SETS
packages/core/src/agent/tools/dynamic-tools.tsMeta-tools: search_tools, get_tool_help, use_tool, create_tool
packages/core/src/agent/tools/tool-tags.tsHidden search tags for tool discovery
packages/core/src/agent/tools/tool-limits.tsMaximum parameter limits for list-returning tools
packages/core/src/agent/retry.tswithRetry() exponential backoff utility
packages/core/src/agent/debug.tsRequest/response logging, payload breakdown
packages/core/src/agent/presets.tsAgent presets
packages/gateway/src/routes/agents.tsAgent CRUD routes, runtime agent creation, tool registration, caching
packages/gateway/src/db/repositories/agents.tsAgentsRepository database access
packages/gateway/src/db/seeds/default-agents.tsDefault agent loader from JSON

Download now (ZIP)

  • Where files are headingNew AGENTS.md files have more rules about keeping secrets out of the code, and fewer about code style.

    Of the AGENTS.md files we read, the 28,693 first seen in the last six months, against the older ones: the kind of rule that gained most share, and the kind that lost most. About files like this in general, not this one, and an estimate, since a kind is a pattern in the words.

  • In this repository3 other files it gives its agents.

Does it work?

Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.

No reviews yet

People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.

  • It worked
  • Partly
  • It didn't
What did you use it for? e.g. Reviewing pull requests in a Rails app

Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.

Your trail

Files you open will collect here, so you can find them again.

Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.

Now do this for your own.

Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.