Technical Documentation

Custom Agents

An open-source, terminal-native AI coding assistant. Like Claude Code — but you own it. Runs any OpenAI-compatible model. 5 specialized agents, 35+ tools, parallel teams, persistent kanban, and a plugin system built for extensibility.

Runtime Bun
Language TypeScript (strict)
UI React 18 + Ink
Tools 35+
Agents 5 built-in + custom
LLM API Any OpenAI-compatible
01

What is Custom Agents?

Custom Agents is a fully terminal-native AI coding assistant. You interact with it through your terminal — there is no browser, no IDE plugin, and no cloud dashboard. The application renders its interactive UI using React + Ink, which renders React components directly inside the terminal instead of a browser DOM.

At its heart, Custom Agents is a tool-calling AI loop. You send a natural language instruction; the AI thinks, selects tools, executes them (reading files, running shell commands, searching the web, editing code), reads the results, and continues until it has a complete answer or solution. This loop is orchestrated through the Query Loop — the core engine of the entire system.

What distinguishes Custom Agents from simpler AI assistants is its multi-agent architecture. Instead of a single monolithic AI, it ships five specialized agents (Explorer, Coder, Reviewer, Documenter, Architect), each with scoped tool access and tuned system prompts. These agents can also be spawned in parallel into agent teams that communicate through an in-memory mailbox and coordinate work through a shared task graph with dependency tracking.

Key mental model: Think of Custom Agents as a local engineering team in a box. You are the product manager — you describe what you need. The lead agent routes your request to the right specialist(s), they use real tools on your real codebase, report back, and you see the work happen in real time in your terminal.

Problems It Solves

Without Custom AgentsWith Custom Agents
Copy-paste code into ChatGPT — lose all file contextAgent reads your actual files and understands your full project structure
Manually apply AI-suggested edits to your filesAgent writes and edits files directly, showing inline diffs
Switch constantly between browser and terminalEverything happens in your terminal — no context switching
One generic AI for everythingSpecialized agents tuned for exploration, coding, review, docs, architecture
Run one task at a timeSpawn parallel agent teams that coordinate and share a task board
Vendor lock-in to a single providerUse any OpenAI-compatible API — OpenRouter, Ollama, LM Studio, OpenAI, etc.
02

System Architecture

The system is organized into twelve clearly bounded modules. Each module owns a distinct responsibility, and they interact through well-defined interfaces — TypeScript types, async functions, and event hooks — rather than direct coupling. Below is the high-level architecture map.

TERMINAL UI (Ink + React) screens/ · components/ · hooks/ ENTRYPOINT / INIT entrypoints/init.ts · state/ QUERY LOOP query/query.ts · streamOpenAI.ts compaction.ts · orchestration.ts AGENT ROUTER agents/AgentRouter.ts builtinAgents · customAgentStore TOOL REGISTRY tools/registry.ts 35+ tools · orchestration LLM API (OpenAI-compat) OpenRouter · Ollama · OpenAI TEAMS TeamManager Mailbox · Promise.allSettled TASK MANAGER TaskManager.ts deps · claim · transitions KANBAN BOARD KanbanStore.ts cards · tasks · columns SKILLS builtinSkills.ts customSkillStore MODELS ModelProfileStore resolveModelConfig MEMORY MemoryStore · 3 scopes PERSISTENCE SessionPersistence · transcripts HOOKS HookManager · lifecycle events PLUGINS tools · hooks · skills Core path LLM API call Indirect / support dependency Central engine Support module

Module Responsibilities

ModulePathResponsibility
Query Loopsrc/query/The core AI loop — streams tokens from the LLM, executes tool calls, manages turns, triggers compaction
Tool Registrysrc/tools/Registers all 35+ tools, converts them to OpenAI function-call schema, dispatches execution
Agent Routersrc/agents/Resolves agent names to definitions, manages built-in and custom agent store
Teamssrc/teams/Parallel multi-agent execution, teammate state tracking, in-memory mailbox
Task Managersrc/tasks/In-memory task graph with status transitions, dependency tracking, atomic claiming
Kanban Boardsrc/kanban/File-backed persistent Kanban board; cards, sub-tasks, columns, priority, labels
Skillssrc/skills/Slash command system — built-in and user-created custom slash commands
Memorysrc/memory/File-backed key-value memory across three scopes: project, user, session
Persistencesrc/persistence/Saves and loads session transcripts (conversation history) to/from disk
Modelssrc/models/Per-agent model profile store — lets different agents use different LLMs
Hookssrc/hooks/Lifecycle event system — emit and subscribe to events like query:before, team:start
Pluginssrc/plugins/Extensibility layer for loading third-party tools, hooks, and skills
Terminal UIsrc/components/ · src/screens/All terminal rendering using React + Ink — REPL screen, streaming output, team status panel
03

End-to-End Data Flow

Every interaction follows a consistent path from keyboard input to final terminal output. Understanding this path is key to understanding how all modules connect in practice.

① USER INPUT Keyboard → Ink REPL screen screens/ · /slash detection ② SLASH / SKILL CHECK Skills router matches /command Expands promptTemplate → user msg ③ AGENT RESOLUTION AgentRouter resolves definition builtin or custom · system prompt ④ QUERY LOOP ★ Compaction check → stream LLM Parse tool_calls → execute tools Append results → loop / finish ⑤ RENDER OUTPUT Ink components stream to terminal Diffs · tool names · final answer LLM STREAMING streamOpenAI → POST /v1/chat/completions SSE chunks → onToken callback Accumulates assistant message TOOL EXECUTION orchestration.ts: fan-out per tool_call shell / file_read / grep / web_search… Returns ToolResultMessage Appended to message array → next turn CONTEXT COMPACTION Estimate tokens each turn ≥80% budget → compact pipeline Truncate → Collapse → Summarize MEMORY INJECTION MemoryStore.buildContext() Appended to system prompt project + user + session scopes SESSION PERSISTENCE SessionPersistence → JSON on disk Reload between sessions HOOK EVENTS query:before/after · message:assistant team:start/complete · tool events
1Keyboard input parsed by Ink REPL
2Slash command or plain query
3Agent + tools resolved
4Query loop runs N turns
5Terminal renders answer
04

The Query Loop — Core Engine

The query loop (src/query/query.ts) is the central engine of the entire system. Every agent, every team member, and every slash command ultimately runs through runQueryLoop(). It implements the agentic AI loop: stream tokens from an LLM, detect tool calls, execute them, feed results back, and repeat until the model produces a final answer or the turn limit is reached.

runQueryLoop() Build system prompt + inject memory Prepend system message · emit query:before hook WHILE turnCount < maxTurns AND NOT aborted compactMessages() estimate tokens → if ≥80% budget, compact streamChatCompletion() POST /v1/chat/completions → SSE stream → onToken() tool_calls? in response YES executeToolCalls() fan-out per call append results loop NO → final answer return QueryResult

Key Implementation Details

The loop receives an abortSignal so any turn can be cancelled — pressing Ctrl+C sends the abort, and the loop exits cleanly at the next check point. The setAppState function wires streaming tokens directly to React state, so every token appears in the terminal the moment it arrives from the API.

Tool calls are executed through executeToolCalls() in src/tools/orchestration.ts. Multiple tool calls in a single assistant message are dispatched in a loop, with their results all appended before the next LLM turn begins. This means the model sees all tool results at once on the next turn, which is the most common multi-tool pattern.

Max turns guard: The loop has a hard ceiling defined by config.maxTurns (default: 20). Individual agents override this — Explorer caps at 8 turns, Coder at 15. This prevents runaway loops burning tokens on unresolvable tasks.

05

Streaming & Context Compaction

Streaming

The streamChatCompletion() function in src/query/streamOpenAI.ts connects to any OpenAI-compatible endpoint using Server-Sent Events (SSE). Each chunk that arrives is passed to an onToken callback, which updates React state, causing Ink to re-render the terminal output in real time. The full assistant message is accumulated from all chunks and returned when the stream ends.

Context Compaction

LLMs have finite context windows. Custom Agents tracks a configurable context budget (default: 120,000 tokens). Before each LLM turn, it estimates the total tokens in the message array using a conservative approximation (~3.5 chars per token). When usage reaches 80% of the budget, the compaction pipeline triggers automatically.

Compaction applies three strategies in order, stopping as soon as the message array fits under budget:

Strategy 1: Truncate

Shorten long tool result contents in older messages. Each tool result is capped to 200 characters with a truncation marker. Fast and lossless for tool call identity.

Strategy 2: Collapse

Replace old assistant-message + tool-result pairs with a single compact system note summarizing what happened. Preserves the fact that a tool was called without keeping its full output.

Strategy 3: Summarize

Drop the oldest non-system messages entirely, replaced by a single [Earlier conversation compacted] marker. A last resort that guarantees the budget is met.

Users can also trigger compaction manually at any time with the /compact slash command, which forces the pipeline to run regardless of the current budget level.

06

Tool Registry — 35+ Built-in Tools

Every capability an agent has comes from tools. Tools are TypeScript classes that extend a base Tool interface, declare a JSON Schema for their inputs, and implement an execute() method. The Tool Registry (src/tools/registry.ts) collects all registered tools, converts them to OpenAI function-call format, and dispatches execution by name.

CategoryToolsPurpose
File Operations file_read · file_write · file_edit Read file contents, write new files, apply targeted text edits with inline diff display
Search grep · glob · tool_search Regex search through file contents, file pattern matching, discover available tool capabilities
Shell shell Execute any shell command — run tests, git operations, builds, arbitrary scripts
Web web_search · web_fetch Search the web, fetch URLs — look up docs, APIs, library references
Task Management task_create · task_list · task_get · task_update · task_stop · task_output Create and track background tasks with status, dependencies, and captured output
Agent Orchestration agent_spawn · agent_create Spawn a sub-agent with a task message; create and persist custom agent definitions
Team Coordination team_create · team_status · team_message · team_check_messages · team_task_claim Form agent teams, broadcast/receive mailbox messages, atomically claim tasks
Kanban Board kanban (multi-action) Add/move/update/archive cards; add/toggle/remove sub-tasks; view full board state
Code Quality lsp_diagnostics · notebook_edit · repl Check TypeScript/language server diagnostics, edit Jupyter notebooks, execute REPL code
Skills & Mode skill_create · skill_list · enter_plan_mode · exit_plan_mode · brief_toggle Persist custom slash commands, control plan mode, toggle compact output
Misc ask_user_question · sleep · todo_write · config Interactive prompts, delays, simple todo tracking, configuration inspection

Scoped registries: Agents don't receive all tools — only the subset declared in their allowedTools list. This is enforced at registry build time in buildTeammateRegistry(), so a Reviewer agent cannot accidentally call file_write even if the user asks it to.

07

Five Built-in Agents

Each agent is an AgentDefinition object — a data structure declaring its name, system prompt, allowed tools, max turns, and optional model profile. The AgentRouter resolves agent names to these definitions at runtime. Agents are not classes; they are pure configuration that parameterizes the shared query loop.

🔍 Explorer
Quick codebase exploration and search. Reads files and searches with grep/glob but never modifies them. Best for answering "where is X?" or "how does Y work?" questions.
grepglobfile_readshellweb_searchweb_fetchtool_searchkanban
⏱ 8 max turns📖 read-only
⚡ Coder
Code generation and editing. Full read/write access. Follows a structured workflow: read → understand → edit → verify with LSP and tests. The most capable and trusted agent.
grepglobfile_readfile_writefile_editshelllspreplweb_*kanban
⏱ 15 max turns✏️ read + write
🔎 Reviewer
Code review and analysis. Examines code for bugs, security issues, performance, and style. Uses LSP diagnostics to surface type errors. Returns specific, actionable feedback. Read-only.
grepglobfile_readshelllsp_diagnosticsweb_searchkanban
⏱ 10 max turns📖 read-only
📝 Documenter
Technical documentation generation. Writes READMEs, API docs, architecture overviews, inline comments, and changelogs. Can write files. Reads the project structure to produce accurate docs.
grepglobfile_readfile_writefile_editshellweb_*kanban
⏱ 12 max turns✏️ read + write
🏗️ Architect
Architecture analysis and design. Maps module boundaries, data flows, dependency graphs, and extension points. Produces analysis and plans — never modifies files. Great for design decisions.
grepglobfile_readshelllspweb_*tool_searchkanban
⏱ 12 max turns📖 read-only

How to Invoke Agents

Agents are invoked through the agent_spawn tool (from within other agents or the lead), or directly via the terminal. The system detects agent-intent keywords in your message. You can also specify an agent explicitly:

# Invoke specific agent for a task
> As the coder agent, implement a rate limiter for the API endpoint

# Spawn a sub-agent from the lead agent (tool call)
agent_spawn({ agent: "explorer", task: "Map all imports in src/query/" })
08

Custom Agents

Beyond the five built-in agents, you can define your own agents with arbitrary tool sets and system prompts. Custom agents are created through the /agent slash command and persisted to disk via customAgentStore.ts — they survive across sessions and become available immediately after creation.

When you type /agent create a test runner agent that runs Jest and summarizes failures, the system uses the AgentSkill's prompt template to instruct the LLM to generate a complete AgentDefinition object and call agent_create with it. The definition is saved to ~/.custom-agents/agents.json.

// Stored structure of a custom agent (agents.json)
{
  "name": "test-runner",
  "description": "Runs Jest tests and summarizes failures",
  "systemPrompt": "You are a test execution agent...",
  "allowedTools": ["shell", "file_read", "grep"],
  "maxTurns": 8,
  "mode": "sync",
  "modelProfile": "fast"   // optional: routes to a specific model
}

Custom agents optionally reference a model profile by name. This lets you route lightweight agents to cheaper/faster models (like GPT-4o Mini) while keeping complex agents on powerful models (like Claude Opus or GPT-4o).

09

Agent Teams — Parallel Multi-Agent Execution

The most powerful feature of Custom Agents is the ability to form agent teams — groups of specialized agents that work in parallel, each on their own task, communicating through a shared mailbox and coordinating via a task graph with dependency tracking.

LEAD AGENT team_create → TeamManager.run() Promise.allSettled() concurrent 🔍 Explorer Teammate scoped registry: read-only isolated AppState store team_message · team_check_messages team_task_claim ⚡ Coder Teammate scoped registry: read + write isolated AppState store team_message · team_check_messages team_task_claim 🔎 Reviewer Teammate scoped registry: read-only isolated AppState store team_message · team_check_messages team_task_claim IN-MEMORY MAILBOX send · broadcast · peek · receive TaskManager: root + child tasks blockedBy/blocks · atomic claim Promise.allSettled → collect outputs Lead synthesizes → final response

How Teams Work Internally

When the lead agent calls team_create, the TeamManager creates a TeamState record with a shared Mailbox instance, a root task for the team, and one child task per teammate. Each teammate gets its own isolated AppState store — so streaming state, tool call tracking, and turn counts are completely independent per agent.

The actual concurrent execution is a single Promise.allSettled() call that runs all teammates simultaneously. Because Bun is single-threaded with an async event loop, there are no race conditions on the shared mailbox — all async operations interleave safely without locks.

Task Claiming with Dependencies

The TaskManager supports dependency chains: task A can declare that it blockedBy task B. When B completes, B is automatically removed from A's blockedBy array, making A claimable. The claim() method is atomic — it checks status, claimedBy, and blockedBy in a single synchronous operation, preventing two teammates from claiming the same task.

// Example: spawn a team from a terminal prompt
> Create a team with an explorer and a coder to refactor src/query/

// Internally, the lead agent calls:
team_create({
  name: "query-refactor",
  teammates: [
    { agent: "explorer", task: "Map the query module structure" },
    { agent: "coder", task: "Refactor compaction.ts to reduce coupling" }
  ]
})
10

Inter-Agent Mailbox

The Mailbox class (src/teams/Mailbox.ts) provides in-process pub/sub messaging between teammates in a team. It is the only shared mutable object between concurrent teammates — and since Bun's event loop is single-threaded, all operations are naturally race-condition-free.

send(from, to, content)

Send a direct message to a specific teammate by ID. The message is stored and the target's subscribers are notified. Sets read: false initially.

send(from, "all", content)

Broadcast to all other teammates. All subscribers except the sender receive the notification. Useful for status updates — "I've finished mapping the module."

receive(agentId)

Fetch all unread messages addressed to this agent (direct or broadcast), marking them as read. Teammates call this via the team_check_messages tool.

peek(agentId)

Read unread messages without marking them as read. Useful for checking if there are pending messages before deciding whether to call receive.

Message structure: Each message has an id, from, to (or "all"), content string, timestamp, and read boolean. The full history() is always accessible for debugging or audit purposes.

11

Slash Commands (Skills System)

Slash commands are implemented as Skills — lightweight configuration objects that define a /name, a description, and either a promptTemplate (type: "prompt") or a direct tool invocation (type: "tool"). When you type /explain my function, the skills router matches the command, expands the promptTemplate with your input substituted for {"{{input}}"}, and feeds the result to the query loop as if you had typed that expanded prompt manually.

CommandTypeWhat It Does
/explainpromptGenerates a detailed explanation of the provided code — patterns, behavior, edge cases
/commitpromptRuns git diff --cached, writes a conventional commit message for staged changes
/statuspromptRuns git status and recent log, gives a concise project status summary
/findpromptUses grep + glob to search the codebase for files or code matching the query
/difftoolShows an inline side-by-side diff of all uncommitted changes
/compacttoolForces context compaction immediately, freeing up token budget
/planpromptEnters plan mode — agent explores and plans before making any edits
/briefpromptToggles brief/compact output mode for more concise terminal responses
/agentpromptCreates a custom agent from a natural language description, persists it
/skillpromptCreates a custom slash command from a natural language description, persists it
/boardpromptViews and manages the Kanban board — add cards, move columns, run/execute card work

Custom Skills

You can create your own slash commands with /skill create a /lint command that runs ESLint and summarizes the errors. The system generates a SkillDefinition and calls skill_create, saving it to ~/.custom-agents/skills.json. The new /lint command is immediately available in the current and all future sessions.

12

Kanban Board

The Kanban board is a file-backed persistent project board stored at <dataDir>/kanban.json. It gives agents and users a shared visual representation of work items. Unlike the in-memory TaskManager (which tracks ephemeral agent tasks), the Kanban board persists across sessions and represents project-level work items.

BACKLOG Add auth tests medium priority 0/3 tasks done PLANNING RAG design high priority 0/5 tasks done IN-PROGRESS Refactor query.ts high · coder agent 2/4 tasks done ↑ real-time progress REVIEW Types cleanup low priority 4/4 tasks done ✓ DONE Install script completed Base tool registry completed

Board Schema

Each card has a title, optional description, column (backlog | planning | in-progress | review | done), priority (low | medium | high), labels array, and a list of sub-tasks. Sub-tasks are lightweight checklist items with a done boolean. When an agent works on a card, it toggles sub-tasks as it completes each step, providing real-time progress visible to the user via /board.

Agent-Driven Card Execution

The /board run "Refactor query.ts" flow is handled by the BoardSkill prompt template. It instructs the AI to: find the card → move it to in-progress → create sub-tasks → spawn the appropriate agent with the card ID and task IDs embedded in the message → verify tasks are toggled → move the card to done. The spawned agent marks sub-tasks complete using kanban tool action toggle_task as it works.

13

Task Management

The TaskManager (src/tasks/TaskManager.ts) is an in-memory task graph — separate from the Kanban board. Where Kanban tracks persistent project work, TaskManager tracks ephemeral agent execution tasks that live only within a session. It is used primarily by the team system to track teammate execution state and coordinate work distribution.

Task State Machine

Ppending
Rrunning
completed
Ppending
!failed

Tasks support dependency tracking: a task can declare it is blockedBy a list of other task IDs. When a blocking task transitions to completed, the TaskManager automatically removes it from all blocked tasks' blockedBy arrays, making those tasks eligible for claiming. The claim() method is a synchronous atomic check — if the task is already claimed or blocked, it returns null.

Parent-child hierarchy: When a team is created, a root task is created for the team, and child tasks are created for each teammate. This lets TaskManager.list({ parentId }) quickly retrieve all tasks belonging to a team, and the team's completion is derived from transitioning the root task.

14

Memory & Session Persistence

Three-Scope Memory Store

The MemoryStore (src/memory/index.ts) provides file-backed key-value storage across three scopes that differ in lifetime and visibility:

project

Shared across all sessions in the same project directory. Persists until explicitly deleted. Ideal for storing project-specific facts (architecture decisions, known APIs).

~/.custom-agents/memory/project/
user

Shared across all projects for this user. Persists globally. Good for personal preferences — code style, preferred patterns, shortcuts.

~/.custom-agents/memory/user/
session

Scoped to the current session ID. Cleaned up when the session ends. Useful for temporary scratchpad storage within a single working session.

~/.custom-agents/memory/session/{id}/

Memory is injected into the system prompt via MemoryStore.buildContext(), which assembles a --- Persistent Memory --- section appended to the agent's system prompt before each query loop. Agents can read and write memory using tools, giving them a form of long-term learning across conversations.

Session Persistence (Transcript)

The SessionPersistence module (src/persistence/SessionPersistence.ts) saves the full conversation transcript — every message, including tool calls and results — to a JSON file on disk. When you start a new session, you can resume a previous one, and the entire history is reloaded into the query loop's message array.

15

Multi-Model Orchestration

By default, all agents use the global model configured in ~/.custom-agents/config.env. But you can assign different LLM models to different agents through model profiles stored in .custom-agents/models.json. This lets you optimize for cost and capability simultaneously: route lightweight explorer tasks to a cheap fast model, while complex coding tasks go to a premium model.

// .custom-agents/models.json
{
  "version": 1,
  "profiles": [
    {
      "name": "fast",
      "model": "openai/gpt-4o-mini",
      "apiKey": "sk-or-v1-...",
      "baseUrl": "https://openrouter.ai/api/v1"
    },
    {
      "name": "reasoning",
      "model": "anthropic/claude-opus-4",
      "apiKey": "sk-or-v1-...",
      "baseUrl": "https://openrouter.ai/api/v1"
    }
  ]
}

The resolveModelConfig() function in src/models/resolveModelConfig.ts is called for each teammate before its query loop starts. It checks the agent definition's optional modelProfile field, looks it up in the ModelProfileStore, and returns the resolved model, apiKey, and baseUrl. If no profile is set, the global config is used — so existing setups require no changes.

16

Plugin System & Hook Events

Hooks — Lifecycle Events

The HookManager (src/hooks/index.ts) is a typed async event bus. Every major lifecycle event emits a hook, and any module (including plugins) can subscribe to intercept or react to those events without modifying core code.

Hook EventWhen It FiresPayload
query:beforeBefore the first LLM turn in a query{ messages }
query:afterAfter query loop completes or errors{ messages, turnCount, error? }
message:assistantEach time the assistant produces a message{ message }
team:startWhen a team begins execution{ teamId, name }
team:completeWhen all teammates finish{ teamId, status, duration }
team:teammate:startWhen an individual teammate begins{ teamId, teammateId, agentName }
team:teammate:endWhen a teammate finishes or fails{ status, output }

Plugin System

The plugins/ module provides the extensibility layer. A plugin can register additional tools (extending the tool registry), subscribe to hooks (adding side effects like logging or notifications), or add new skills (custom slash commands). Plugins are loaded during initialization before the REPL starts, making all their additions available immediately.

17

Configuration

Configuration is read from two sources in priority order: a project-level .env file in the current working directory (highest priority), falling back to the global config at ~/.custom-agents/config.env. This lets you override the model or API key per project without touching global settings.

# ~/.custom-agents/config.env
OPENAI_API_KEY=sk-your-key-here
OPENAI_BASE_URL=https://openrouter.ai/api/v1
MODEL=openrouter/auto
LOG_LEVEL=info        # debug | info | warn | error
MAX_TURNS=20           # global default; per-agent overrides apply
CONTEXT_BUDGET=120000  # tokens before compaction triggers
VariableDescriptionDefault
OPENAI_API_KEYAPI key for your LLM provider
OPENAI_BASE_URLAPI endpoint — any OpenAI-compatible URLhttps://openrouter.ai/api/v1
MODELModel identifier string for your provideropenrouter/auto
LOG_LEVELLogging verbosityinfo
MAX_TURNSGlobal max turns per query loop20
CONTEXT_BUDGETToken budget before compaction120000

Supported LLM Providers

OpenRouter

Access 100+ models through a single API. Set OPENAI_BASE_URL=https://openrouter.ai/api/v1. Recommended for flexibility.

OpenAI

Direct connection to GPT-4o, GPT-4o Mini, and other OpenAI models.

Ollama / LM Studio

Fully local, offline operation. No data leaves your machine. Set base URL to localhost.

18

Installation & Development

One-Liner Install

# Install (or update)
curl -fsSL https://raw.githubusercontent.com/iabhisekbosepm/custom_agent/main/install.sh | bash

# The installer will:
# 1. Install Bun if not present
# 2. Clone source to ~/.custom-agents-cli/
# 3. Prompt for OPENAI_API_KEY, OPENAI_BASE_URL, MODEL
# 4. Register the 'custom-agents' command globally

# Then use it anywhere
cd ~/any-project
custom-agents

Development Setup

git clone https://github.com/iabhisekbosepm/custom_agent.git
cd custom_agent
cp .env.example .env      # Add your API key
bun install               # Install dependencies
bun run src/index.ts      # Start
bun --watch run src/index.ts  # Dev mode with hot reload
bun test                  # Run tests
bun x tsc --noEmit        # Type check
19

Tech Stack

Bun — Runtime

Replaces Node.js. Faster startup, native TypeScript support, built-in Bun.file() and Bun.write() APIs. Test runner included. The single-threaded async event loop makes in-memory concurrency safe without locks.

React 18 + Ink — Terminal UI

Ink renders React components as terminal output using flexbox layout. State updates (e.g., a streaming token) trigger a React re-render which updates the terminal in-place — just like a browser SPA, but in your terminal.

TypeScript (strict) — Language

Full strict mode. All messages, tool inputs/outputs, agent definitions, and state objects are typed. Zod is used for runtime validation of tool inputs before execution.

OpenAI API (compatible) — LLM

The entire system is built around the OpenAI /v1/chat/completions streaming API, function-calling schema, and tool result messages. Any provider that implements this interface works — OpenRouter, Ollama, LM Studio, etc.

Project Structure (annotated)

src/
├── agents/        # AgentDefinition type, AgentRouter, 5 builtin agents, customAgentStore
├── components/    # Ink terminal UI: message bubbles, streaming, diffs, team panel
├── entrypoints/   # CLI entry point, initialization, dependency wiring
├── hooks/         # HookManager: typed async lifecycle event bus
├── kanban/        # KanbanStore: file-backed board, cards, sub-tasks
├── memory/        # MemoryStore: 3-scope key-value file storage
├── models/        # ModelProfileStore: per-agent LLM config, resolveModelConfig
├── persistence/   # SessionPersistence: save/load conversation transcripts
├── plugins/       # Extensibility: load tools/hooks/skills from plugins
├── query/         # Core engine: runQueryLoop, streamOpenAI, compaction
├── screens/       # Terminal screens: REPL input, session manager
├── services/      # Background services startup/shutdown
├── skills/        # SkillDefinition: 11 builtin slash commands, customSkillStore
├── state/         # AppStateStore: reactive state; store.ts: createStore()
├── tasks/         # Task.ts: state machine; TaskManager: graph, deps, claiming
├── teams/         # TeamManager, Mailbox, TeamTypes, buildTeammateRegistry
├── tools/         # 35+ tool implementations (one dir per tool), registry, orchestration
├── types/         # Shared types: messages, config, etc.
└── utils/         # logger, diff renderer, env loader, id generator, shutdown