Skip to main content

Level 1 — Framework

Write a code-based agent on top of @codebolt/agent. In practice, level 1 today means the unified API in @codebolt/agent/unified plus reusable modifiers and processors from @codebolt/agent/processor-pieces.

This is the default choice for real custom agents. You write TypeScript and customize the prompt and processor pipeline, while the framework handles the prompt -> LLM -> tool execution -> follow-up loop.

When level 1 is the right choice

  • You need custom code, not just a prompt remix.
  • You want to add, remove, or reorder context modifiers and processors.
  • You need loop detection, compaction, or tool-call hooks.
  • You want a normal Codebolt agent loop without hand-writing the whole thing.
  • Level 0 is too limited, but level 2 would be unnecessary control.

If you need complete ownership of the loop and runtime wiring, skip to level 2.

What the framework actually gives you

At the high level, CodeboltAgent.processMessage() does four things:

  1. Builds the initial prompt with InitialPromptGenerator.
  2. Runs an LLM step with AgentStep.
  3. Executes tool calls and builds the next prompt with ResponseExecutor.
  4. Repeats until the run completes or maxTurns is hit.

Optional runtime features in the current implementation:

  • Default message modifiers if you do not provide your own.
  • Tool refresh before each turn.
  • Loop detection via LoopDetectionService.
  • Conversation compaction and recovery via compaction.

This is a concrete runtime API, not a planned one. You do not export default createCodeboltAgent(...) as the agent contract. The normal pattern is to register a codebolt.onMessage(...) handler and call processMessage(...) inside it.

The normal entry point

A level-1 agent usually registers codebolt.onMessage(...) and delegates to CodeboltAgent. That is the pattern used by real agents such as act-updated.

import codebolt from '@codebolt/codeboltjs';
import { CodeboltAgent } from '@codebolt/agent/unified';
import { FlatUserMessage } from '@codebolt/types/sdk';

const agent = new CodeboltAgent({
instructions: 'You are a helpful coding assistant.',
enableLogging: true,
});

codebolt.onMessage(async (reqMessage: FlatUserMessage) => {
const result = await agent.processMessage(reqMessage);

if (!result.success) {
throw new Error(result.error ?? 'Agent failed');
}

return result.finalMessage;
});

The package also exports createCodeboltAgent(...), but it is only a thin convenience wrapper around new CodeboltAgent(...). For anything non-trivial, the class is the clearer surface.

If you do not supply custom messageModifiers, CodeboltAgent installs a default stack:

  • ChatHistoryMessageModifier
  • EnvironmentContextModifier
  • DirectoryContextModifier
  • IdeContextModifier
  • CoreSystemPromptModifier
  • ToolInjectionModifier
  • AtFileProcessorModifier

Customizing the pipeline

CodeboltAgent configuration is centered on instructions plus optional context, allowedTools, enableLogging, maxTurns, compaction, loopDetectionService, and processors.

The real extension points live in processors:

  • messageModifiers
  • preInferenceProcessors
  • postInferenceProcessors
  • preToolCallProcessors
  • postToolCallProcessors

A trimmed version of the act-updated agent looks like this:

import {
CodeboltAgent,
LoopDetectionService,
} from '@codebolt/agent/unified';
import {
EnvironmentContextModifier,
CoreSystemPromptModifier,
DirectoryContextModifier,
IdeContextModifier,
AtFileProcessorModifier,
ToolInjectionModifier,
ChatHistoryMessageModifier,
} from '@codebolt/agent/processor-pieces';

const loopDetectionService = new LoopDetectionService({ debug: true });

const agent = new CodeboltAgent({
instructions: systemPrompt,
enableLogging: true,
loopDetectionService,
maxTurns: 30,
compaction: {
enableLogging: true,
autoCompactEnabled: true,
contextCollapseEnabled: false,
},
processors: {
messageModifiers: [
new ChatHistoryMessageModifier({ enableChatHistory: true }),
new EnvironmentContextModifier({ enableFullContext: false }),
new DirectoryContextModifier(),
new IdeContextModifier({
includeActiveFile: true,
includeOpenFiles: true,
includeCursorPosition: true,
includeSelectedText: true,
}),
new CoreSystemPromptModifier({ customSystemPrompt: systemPrompt }),
new ToolInjectionModifier({ includeToolDescriptions: true }),
new AtFileProcessorModifier({ enableRecursiveSearch: true }),
externalEventProcessor,
],
postToolCallProcessors: [externalEventPostToolProcessor],
},
});

This is the core level-1 pattern: keep the framework loop, but swap in your own prompt assembly and processing hooks. In act-updated, custom processors inject external steering and queued events between turns.

When CodeboltAgent is not enough

You can drop one layer lower without leaving level 1. The framework exposes its building blocks directly:

  • InitialPromptGenerator
  • AgentStep
  • ResponseExecutor

Use them when you need to change the turn logic but still want framework components instead of raw @codebolt/codeboltjs.

const prompt = await promptGenerator.processMessage(reqMessage);
const stepResult = await agentStep.executeStep(reqMessage, prompt);
const execution = await responseExecutor.executeResponse({
initialUserMessage: reqMessage,
actualMessageSentToLLM: stepResult.actualMessageSentToLLM,
rawLLMOutput: stepResult.rawLLMResponse,
nextMessage: stepResult.nextMessage,
});

If you need to own the entire loop, event wiring, or low-level SDK interaction yourself, move to level 2.

Carrying context across calls

processMessage() returns the processed conversation context, so you can continue a run in a later call:

const firstResult = await agent.processMessage(reqMessage);

const followUpAgent = new CodeboltAgent({
instructions: systemPrompt,
context: firstResult.context ?? undefined,
});

const secondResult = await followUpAgent.processMessage(
'Continue from the previous result.'
);

Processing external events

While an agent is running, external events can arrive — steering instructions from the user, inter-agent messages, or background agent completions. Use codebolt.agentEventQueue to poll for these and inject them into the prompt so the LLM sees them on the next turn.

The event queue

const eventQueue = codebolt.agentEventQueue;

// Returns pending events and clears them from the queue
const events = eventQueue.getPendingExternalEvents();

Event types

Event typeSourceWhat it means
agentQueueEvent with payload.type === 'steering'User sent a message while the agent is workingRedirect the agent's current approach
agentQueueEvent (other)Another agent sent a messageInter-agent communication
backgroundAgentCompletionA background agent finishedNotify the LLM about completed background work

Injecting events as a message modifier

Create a custom processor that polls the queue and pushes events into the prompt's message array:

import { ProcessedMessage } from '@codebolt/types/agent';
import { FlatUserMessage } from '@codebolt/types/sdk';

const eventQueue = codebolt.agentEventQueue;

function processExternalEvent(event: any, prompt: ProcessedMessage): ProcessedMessage {
if (!event || !prompt?.message?.messages) return prompt;

const eventType = event.type || event.eventType;
const eventData = event.data || event;

if (eventType === 'agentQueueEvent') {
const payload = eventData?.payload || {};

// Steering: user sent a follow-up while the agent is working
if (payload.type === 'steering') {
const instruction = payload.instruction || payload.content || JSON.stringify(payload);
prompt.message.messages.push({
role: 'user' as const,
content: `<steering_message>
<instruction>${instruction}</instruction>
<context>The user has sent a steering message while you are working. Adjust your approach accordingly.</context>
</steering_message>`,
});
return prompt;
}

// Inter-agent message
const content = payload.content || JSON.stringify(payload);
prompt.message.messages.push({
role: 'user' as const,
content: `<agent_event>
<source>${eventData.sourceAgentId || 'system'}</source>
<content>${content}</content>
</agent_event>`,
});
return prompt;
}

if (eventType === 'backgroundAgentCompletion') {
prompt.message.messages.push({
role: 'assistant' as const,
content: `Background agent completed:\n${JSON.stringify(eventData, null, 2)}`,
});
return prompt;
}

return prompt;
}

Wiring into CodeboltAgent

Register the processor in two slots so events are picked up both during initial prompt assembly and after tool calls:

// As a message modifier — runs during prompt assembly
const externalEventProcessor = {
async modify(_originalRequest: FlatUserMessage, createdMessage: ProcessedMessage) {
let prompt = createdMessage;
for (const event of eventQueue.getPendingExternalEvents()) {
prompt = processExternalEvent(event, prompt);
}
return prompt;
},
};

// As a post-tool-call processor — runs after each tool execution
const externalEventPostToolProcessor = {
async modify({ nextPrompt }: { nextPrompt: ProcessedMessage }) {
let prompt = nextPrompt;
for (const event of eventQueue.getPendingExternalEvents()) {
prompt = processExternalEvent(event, prompt);
}
return { nextPrompt: prompt, shouldExit: false };
},
};

const agent = new CodeboltAgent({
instructions: systemPrompt,
processors: {
messageModifiers: [
// ... your other modifiers ...
externalEventProcessor, // Add at the end of message modifiers
],
postToolCallProcessors: [
externalEventPostToolProcessor, // Check for events after every tool call
],
},
});

This gives the agent two chances to see external events per turn: once when the prompt is assembled and once after each tool call completes.

Creating custom tools

Use the Tool class or createTool helper to define tools with Zod schema validation:

import { Tool, createTool } from '@codebolt/agent/unified';
import { z } from 'zod';

const myTool = createTool({
id: 'analyze-file',
description: 'Analyze a file and return a summary',
inputSchema: z.object({
filePath: z.string().describe('Path to the file'),
detailed: z.boolean().optional().describe('Include detailed analysis'),
}),
outputSchema: z.object({
summary: z.string(),
lineCount: z.number(),
}),
execute: async ({ input }) => {
// Your tool logic here
return { summary: 'File analysis complete', lineCount: 42 };
},
});

The Tool class:

  • Validates inputs against inputSchema (Zod) before execution
  • Validates outputs against outputSchema (Zod) if provided
  • Converts to OpenAI function format via toOpenAITool()
  • Returns { success: boolean, result?, error? } from execute()

Building blocks API

When CodeboltAgent's loop doesn't fit your needs, use the building blocks directly:

InitialPromptGenerator

Runs message modifiers to build the initial prompt:

import { InitialPromptGenerator } from '@codebolt/agent/unified';

const promptGenerator = new InitialPromptGenerator({
processors: myMessageModifiers, // MessageModifier[]
baseSystemPrompt: 'You are ...',
enableLogging: true,
});

const prompt = await promptGenerator.processMessage(reqMessage);

AgentStep

Executes one LLM call with pre/post-inference processors:

import { AgentStep } from '@codebolt/agent/unified';

const agentStep = new AgentStep({
preInferenceProcessors: [],
postInferenceProcessors: [],
});

const stepResult = await agentStep.executeStep(reqMessage, prompt);
// stepResult.rawLLMResponse — the LLM's raw response
// stepResult.nextMessage — the prompt with LLM response appended
// stepResult.actualMessageSentToLLM — what was actually sent to the LLM

ResponseExecutor

Handles tool execution with pre/post-tool-call processors:

import { ResponseExecutor } from '@codebolt/agent/unified';

const responseExecutor = new ResponseExecutor({
preToolCallProcessors: [],
postToolCallProcessors: [],
loopDetectionService: myLoopDetection, // optional
});

const execution = await responseExecutor.executeResponse({
initialUserMessage: reqMessage,
actualMessageSentToLLM: stepResult.actualMessageSentToLLM,
rawLLMOutput: stepResult.rawLLMResponse,
nextMessage: stepResult.nextMessage,
});
// execution.completed — true if LLM produced a final answer
// execution.nextMessage — updated prompt for next turn
// execution.finalMessage — the agent's text response (if completed)

LoopDetectionService

Detects repeated tool calls:

import { LoopDetectionService } from '@codebolt/agent/unified';

const loopDetection = new LoopDetectionService({
toolCallLoopThreshold: 5, // trigger after 5 identical calls (default)
debug: true, // log detection events
});

// Check inside your loop:
const loopEvent = loopDetection.checkToolCallLoop(toolName, toolArgs);
if (loopEvent) {
console.warn('Loop detected:', loopEvent);
}

// Reset between runs:
loopDetection.reset();

codeboltagent.yaml reference

The manifest describes your agent to the UI and routing system:

# Required
title: My Agent
version: 1.0.0
unique_id: my-agent
description: What this agent does in one line.

# Optional identity
initial_message: Hi! I'm ready to help.
tags: [coding, typescript]
longDescription: |
A longer description shown on the agent detail page.
avatarSrc: https://example.com/icon.png
avatarFallback: MA
author: your-name
supportRemix: true

# Agent routing — tells Codebolt when to suggest this agent
metadata:
agent_routing:
worksonblankcode: true
worksonexistingcode: true
supportedlanguages: [javascript, typescript, python]
supportedframeworks: [react, node, express]
supportRemix: true

# Default LLM configuration
defaultagentllm:
strict: true
modelorder:
- claude-sonnet-4-20250514
- gpt-4-turbo

# Specialized LLM roles (optional)
llm_role:
- name: documentationllm
description: LLM for documentation tasks.
strict: true
modelorder: [gpt-4-turbo, gpt-3.5-turbo]
- name: testingllm
description: LLM for testing tasks.
strict: true
modelorder: [gpt-4-turbo]

# Actions — buttons shown in the agent UI
actions:
- name: Execute
description: Run the task.
actionPrompt: Let's build this
- name: Plan
actionPrompt: /plan

# Pre-completion verification (optional)
verification:
enabled: true
checks:
lint: true
test: true
build: true
typecheck: true
timeout: 60000
skipOnNoConfig: true
skipForNonCodeTasks: true
maxRetries: -1 # -1 = keep retrying until fixed

Project layout

my-agent/
├── codeboltagent.yaml
├── package.json
├── tsconfig.json
├── webpack.config.js
├── src/
│ └── index.ts
└── dist/
├── index.js
└── codeboltagent.yaml # copied by webpack

Build with webpack

Most agents use webpack to bundle into a single dist/index.js. Key requirements:

  • Target node — agents run in Node.js
  • Copy codeboltagent.yaml to dist/ — Codebolt reads it from the dist folder
  • Externalize @codebolt packages — they're provided at runtime
// webpack.config.js
const path = require('path');
const CopyPlugin = require('copy-webpack-plugin');

module.exports = {
target: 'node',
mode: 'production',
entry: './src/index.ts',
output: {
path: path.resolve(__dirname, 'dist'),
filename: 'index.js',
libraryTarget: 'commonjs2',
},
resolve: {
extensions: ['.ts', '.js'],
},
module: {
rules: [{ test: /\.ts$/, use: 'ts-loader', exclude: /node_modules/ }],
},
plugins: [
new CopyPlugin({
patterns: [{ from: 'codeboltagent.yaml', to: 'codeboltagent.yaml' }],
}),
],
// Don't bundle Node built-ins
externals: {
'@codebolt/codeboltjs': 'commonjs @codebolt/codeboltjs',
'@codebolt/agent': 'commonjs @codebolt/agent',
},
};

package.json scripts

{
"scripts": {
"build": "npx webpack",
"dev": "npx tsx src/index.ts",
"clean": "rimraf dist"
},
"dependencies": {
"@codebolt/codeboltjs": "^5.1.32",
"@codebolt/agent": "^6.0.0"
},
"devDependencies": {
"copy-webpack-plugin": "^12.0.2",
"ts-loader": "^9.5.0",
"typescript": "^5.4.5",
"webpack": "^5.90.0",
"webpack-cli": "^5.1.4"
}
}

When to graduate to level 2

Move to level 2 when:

  • You need a fully custom agent loop.
  • You need raw @codebolt/codeboltjs APIs the framework does not wrap.
  • You are building infrastructure or orchestration primitives rather than a normal chat agent.

For most code-based agents, level 1 is the right level.

See also