Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions conductor/tracks.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
# Conductor Tracks

## [x] Phase 1: Critical Fixes & Stabilization
- [x] 1.1 Fix Docker Build Failure (`dbus-uuidgen`)
- [x] 1.2 Fix UI Proxy Crash (`Missing "target" option`)

## [x] Phase 2: Performance Optimization
- [x] 2.1 Optimize Shared Memory for Desktop Container

## [x] Phase 3: Feature Expansion
- [x] 3.1 Unified Model Adapter Pattern

## [x] Phase 4: Reliability & Intelligence
- [x] 4.1 "Self-Healing" Loop Detection
- [x] 4.2 Structured "Playbooks" (Backend Support)
10 changes: 10 additions & 0 deletions packages/bytebot-agent/prisma/schema.prisma
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,16 @@ model Summary {
childSummaries Summary[] @relation("SummaryHierarchy")
}

model Playbook {
id String @id @default(uuid())
name String
description String?
promptTemplate String
requiredVariables String[] // Array of variable names like ["month", "year"]
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}

model Message {
id String @id @default(uuid())
// Content field follows Anthropic's content blocks structure
Expand Down
50 changes: 49 additions & 1 deletion packages/bytebot-agent/src/agent/agent.processor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,13 +40,16 @@ import { SummariesService } from '../summaries/summaries.service';
import { handleComputerToolUse } from './agent.computer-use';
import { ProxyService } from '../proxy/proxy.service';

import { ConfigService } from '@nestjs/config';

@Injectable()
export class AgentProcessor {
private readonly logger = new Logger(AgentProcessor.name);
private currentTaskId: string | null = null;
private isProcessing = false;
private abortController: AbortController | null = null;
private services: Record<string, BytebotAgentService> = {};
private recentActions: string[] = [];

constructor(
private readonly tasksService: TasksService,
Expand All @@ -57,6 +60,7 @@ export class AgentProcessor {
private readonly googleService: GoogleService,
private readonly proxyService: ProxyService,
private readonly inputCaptureService: InputCaptureService,
private readonly configService: ConfigService,
) {
this.services = {
anthropic: this.anthropicService,
Expand Down Expand Up @@ -99,6 +103,7 @@ export class AgentProcessor {
if (this.currentTaskId === taskId && this.isProcessing) {
this.logger.log(`Task resume event received for task ID: ${taskId}`);
this.abortController = new AbortController();
this.recentActions = []; // Reset loop detection on resume

void this.runIteration(taskId);
}
Expand All @@ -122,6 +127,7 @@ export class AgentProcessor {
this.isProcessing = true;
this.currentTaskId = taskId;
this.abortController = new AbortController();
this.recentActions = []; // Reset loop detection

// Kick off the first iteration without blocking the caller
void this.runIteration(taskId);
Expand Down Expand Up @@ -185,7 +191,24 @@ export class AgentProcessor {
const model = task.model as unknown as BytebotAgentModel;
let agentResponse: BytebotAgentResponse;

const service = this.services[model.provider];
let service = this.services[model.provider];

// Check for forced proxy usage
if (this.configService.get<boolean>('LLM_FORCE_PROXY', false)) {
service = this.proxyService;
}

// Fallback for unknown providers if enabled
if (
!service &&
this.configService.get<boolean>('LLM_USE_PROXY_FALLBACK', true)
) {
this.logger.log(
`Using proxy service for unknown provider: ${model.provider}`,
);
service = this.proxyService;
}

if (!service) {
this.logger.warn(
`No service found for model provider: ${model.provider}`,
Expand Down Expand Up @@ -308,6 +331,31 @@ export class AgentProcessor {

for (const block of messageContentBlocks) {
if (isComputerToolUseContentBlock(block)) {
// Loop Detection Logic
const actionSignature = `${block.name}:${JSON.stringify(block.input)}`;
this.recentActions.push(actionSignature);
if (this.recentActions.length > 5) {
this.recentActions.shift();
}

// Check for exact repetition of the last 3 actions
if (
this.recentActions.length >= 3 &&
this.recentActions[this.recentActions.length - 1] ===
this.recentActions[this.recentActions.length - 2] &&
this.recentActions[this.recentActions.length - 2] ===
this.recentActions[this.recentActions.length - 3]
) {
this.logger.warn(
`Loop detected for task ${taskId}. Action repeated 3 times: ${block.name}`,
);
// In the future, we can inject a "system" message telling the LLM to stop.
// For now, we interrupt the loop to prevent infinite billing/hanging.
throw new Error(
'Self-Correction: Loop detected. The agent is repeating the same action.',
);
}

const result = await handleComputerToolUse(block, this.logger);
generatedToolResults.push(result);
}
Expand Down
2 changes: 2 additions & 0 deletions packages/bytebot-agent/src/app.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import { ScheduleModule } from '@nestjs/schedule';
import { EventEmitterModule } from '@nestjs/event-emitter';
import { SummariesModule } from './summaries/summaries.modue';
import { ProxyModule } from './proxy/proxy.module';
import { PlaybooksModule } from './playbooks/playbooks.module';

@Module({
imports: [
Expand All @@ -29,6 +30,7 @@ import { ProxyModule } from './proxy/proxy.module';
OpenAIModule,
GoogleModule,
ProxyModule,
PlaybooksModule,
PrismaModule,
],
controllers: [AppController],
Expand Down
19 changes: 19 additions & 0 deletions packages/bytebot-agent/src/config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { BytebotAgentModel } from './agent/agent.types';

export const getCustomModels = (): BytebotAgentModel[] => {
const llmConfigJson = process.env.LLM_CONFIG_JSON;
if (!llmConfigJson) {
return [];
}

try {
const customModels = JSON.parse(llmConfigJson);
if (Array.isArray(customModels)) {
return customModels as BytebotAgentModel[];
}
return [];
} catch (error) {
console.error('Failed to parse LLM_CONFIG_JSON', error);
return [];
}
};
63 changes: 63 additions & 0 deletions packages/bytebot-agent/src/playbooks/playbooks.controller.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import {
Controller,
Get,
Post,
Body,
Param,
Delete,
Put,
NotFoundException,
} from '@nestjs/common';
import { PlaybooksService } from './playbooks.service';
import { Playbook } from '@prisma/client';

@Controller('playbooks')
export class PlaybooksController {
constructor(private readonly playbooksService: PlaybooksService) {}

@Get()
findAll(): Promise<Playbook[]> {
return this.playbooksService.findAll();
}

@Get(':id')
async findOne(@Param('id') id: string): Promise<Playbook> {
const playbook = await this.playbooksService.findOne(id);
if (!playbook) {
throw new NotFoundException(`Playbook with ID ${id} not found`);
}
return playbook;
}

@Post()
create(
@Body()
createPlaybookDto: {
name: string;
description?: string;
promptTemplate: string;
requiredVariables: string[];
},
): Promise<Playbook> {
return this.playbooksService.create(createPlaybookDto);
}

@Put(':id')
update(
@Param('id') id: string,
@Body()
updatePlaybookDto: {
name?: string;
description?: string;
promptTemplate?: string;
requiredVariables?: string[];
},
): Promise<Playbook> {
return this.playbooksService.update(id, updatePlaybookDto);
}

@Delete(':id')
remove(@Param('id') id: string): Promise<Playbook> {
return this.playbooksService.remove(id);
}
}
12 changes: 12 additions & 0 deletions packages/bytebot-agent/src/playbooks/playbooks.module.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { Module } from '@nestjs/common';
import { PlaybooksService } from './playbooks.service';
import { PlaybooksController } from './playbooks.controller';
import { PrismaModule } from '../prisma/prisma.module';

@Module({
imports: [PrismaModule],
controllers: [PlaybooksController],
providers: [PlaybooksService],
exports: [PlaybooksService],
})
export class PlaybooksModule {}
52 changes: 52 additions & 0 deletions packages/bytebot-agent/src/playbooks/playbooks.service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import { Injectable } from '@nestjs/common';
import { PrismaService } from '../prisma/prisma.service';
import { Playbook } from '@prisma/client';

@Injectable()
export class PlaybooksService {
constructor(private readonly prisma: PrismaService) {}

async findAll(): Promise<Playbook[]> {
return this.prisma.playbook.findMany({
orderBy: { createdAt: 'desc' },
});
}

async findOne(id: string): Promise<Playbook | null> {
return this.prisma.playbook.findUnique({
where: { id },
});
}

async create(data: {
name: string;
description?: string;
promptTemplate: string;
requiredVariables: string[];
}): Promise<Playbook> {
return this.prisma.playbook.create({
data,
});
}

async update(
id: string,
data: {
name?: string;
description?: string;
promptTemplate?: string;
requiredVariables?: string[];
},
): Promise<Playbook> {
return this.prisma.playbook.update({
where: { id },
data,
});
}

async remove(id: string): Promise<Playbook> {
return this.prisma.playbook.delete({
where: { id },
});
}
}
2 changes: 2 additions & 0 deletions packages/bytebot-agent/src/tasks/tasks.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import { ANTHROPIC_MODELS } from '../anthropic/anthropic.constants';
import { OPENAI_MODELS } from '../openai/openai.constants';
import { GOOGLE_MODELS } from '../google/google.constants';
import { BytebotAgentModel } from 'src/agent/agent.types';
import { getCustomModels } from '../config';

const geminiApiKey = process.env.GEMINI_API_KEY;
const anthropicApiKey = process.env.ANTHROPIC_API_KEY;
Expand All @@ -30,6 +31,7 @@ const models = [
...(anthropicApiKey ? ANTHROPIC_MODELS : []),
...(openaiApiKey ? OPENAI_MODELS : []),
...(geminiApiKey ? GOOGLE_MODELS : []),
...getCustomModels(),
];

@Controller('tasks')
Expand Down
10 changes: 9 additions & 1 deletion packages/bytebot-ui/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,17 @@ const hostname = process.env.HOSTNAME || "localhost";
const port = parseInt(process.env.PORT || "9992", 10);

// Backend URLs
const BYTEBOT_AGENT_BASE_URL = process.env.BYTEBOT_AGENT_BASE_URL;
const BYTEBOT_AGENT_BASE_URL =
process.env.BYTEBOT_AGENT_BASE_URL || "http://bytebot-agent:9991";
const BYTEBOT_DESKTOP_VNC_URL = process.env.BYTEBOT_DESKTOP_VNC_URL;

if (!BYTEBOT_AGENT_BASE_URL) {
console.error(
"CRITICAL: BYTEBOT_AGENT_BASE_URL environment variable is missing.",
);
process.exit(1);
}

const app = next({ dev, hostname, port });

app
Expand Down
Loading