feat: wire OpenCode as selectable agent backend via AGENT_BACKEND env var
Some checks failed
Update token count / update-tokens (push) Has been cancelled

- Add AGENT_BACKEND config ('container' default, 'opencode' to use OpenCode runtime)
- Wire OpenCodeRuntime into runAgent() with fallback to container runner
- Skip container system check when using OpenCode backend
- Update README and CLAUDE.md with AGENT_BACKEND docs
This commit is contained in:
2026-02-19 02:38:50 -05:00
parent a3a7e7a480
commit 572ccdbd3b
5 changed files with 169 additions and 3 deletions

View File

@@ -1,5 +1,5 @@
<p align="center">
<img src="assets/nanoclaw-logo.png" alt="Regolith" width="400">
<img src="assets/logo.jpeg" alt="Regolith" width="400">
</p>
<p align="center">

BIN
assets/logo.jpeg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 63 KiB

View File

@@ -10,6 +10,7 @@ const envConfig = readEnvFile([
'ASSISTANT_HAS_OWN_NUMBER',
'DISCORD_BOT_TOKEN',
'DISCORD_ONLY',
'AGENT_BACKEND',
]);
export const ASSISTANT_NAME =
@@ -20,6 +21,11 @@ export const DISCORD_BOT_TOKEN =
process.env.DISCORD_BOT_TOKEN || envConfig.DISCORD_BOT_TOKEN || '';
export const DISCORD_ONLY =
(process.env.DISCORD_ONLY || envConfig.DISCORD_ONLY) === 'true';
// Agent backend: 'container' (Claude Agent SDK in containers) or 'opencode' (OpenCode CLI/SDK)
export const AGENT_BACKEND =
(process.env.AGENT_BACKEND || envConfig.AGENT_BACKEND || 'container').toLowerCase() as 'container' | 'opencode';
export const POLL_INTERVAL = 2000;
export const SCHEDULER_POLL_INTERVAL = 60000;

View File

@@ -3,6 +3,7 @@ import fs from 'fs';
import path from 'path';
import {
AGENT_BACKEND,
ASSISTANT_NAME,
DATA_DIR,
DISCORD_BOT_TOKEN,
@@ -16,7 +17,107 @@ import { DiscordChannel } from './channels/discord.js';
import { WhatsAppChannel } from './channels/whatsapp.js';
import {
ContainerOutput,
runContainerAgent,
async function runAgent(
group: RegisteredGroup,
prompt: string,
chatJid: string,
onOutput?: (output: ContainerOutput) => Promise<void>,
): Promise<'success' | 'error'> {
// --- OpenCode backend ---
if (opencode) {
try {
const response = await opencode.chat(prompt, chatJid);
if (response.error) {
logger.error({ group: group.name, error: response.error }, 'OpenCode agent error');
if (onOutput) {
await onOutput({ status: 'error', error: response.error, result: null, newSessionId: response.sessionId });
}
return 'error';
}
if (onOutput && response.text) {
await onOutput({ status: 'success', result: response.text, error: null, newSessionId: response.sessionId });
}
return 'success';
} catch (err) {
logger.error({ group: group.name, err }, 'OpenCode agent error');
return 'error';
}
}
// --- Container backend (Claude Agent SDK) ---
const isMain = group.folder === MAIN_GROUP_FOLDER;
const sessionId = sessions[group.folder];
// Update tasks snapshot for container to read (filtered by group)
const tasks = getAllTasks();
writeTasksSnapshot(
group.folder,
isMain,
tasks.map((t) => ({
id: t.id,
groupFolder: t.group_folder,
prompt: t.prompt,
schedule_type: t.schedule_type,
schedule_value: t.schedule_value,
status: t.status,
next_run: t.next_run,
})),
);
// Update available groups snapshot (main group only can see all groups)
const availableGroups = getAvailableGroups();
writeGroupsSnapshot(
group.folder,
isMain,
availableGroups,
new Set(Object.keys(registeredGroups)),
);
// Wrap onOutput to track session ID from streamed results
const wrappedOnOutput = onOutput
? async (output: ContainerOutput) => {
if (output.newSessionId) {
sessions[group.folder] = output.newSessionId;
setSession(group.folder, output.newSessionId);
}
await onOutput(output);
}
: undefined;
try {
const output = await runContainerAgent(
group,
{
prompt,
sessionId,
groupFolder: group.folder,
chatJid,
isMain,
},
(proc, containerName) => queue.registerProcess(chatJid, proc, containerName, group.folder),
wrappedOnOutput,
);
if (output.newSessionId) {
sessions[group.folder] = output.newSessionId;
setSession(group.folder, output.newSessionId);
}
if (output.status === 'error') {
logger.error(
{ group: group.name, error: output.error },
'Container agent error',
);
return 'error';
}
return 'success';
} catch (err) {
logger.error({ group: group.name, err }, 'Agent error');
return 'error';
}
}
,
writeGroupsSnapshot,
writeTasksSnapshot,
} from './container-runner.js';
@@ -41,6 +142,7 @@ import { findChannel, formatMessages, formatOutbound } from './router.js';
import { startSchedulerLoop } from './task-scheduler.js';
import { Channel, NewMessage, RegisteredGroup } from './types.js';
import { logger } from './logger.js';
import { OpenCodeRuntime } from './opencode/runtime.js';
// Re-export for backwards compatibility during refactor
export { escapeXml, formatMessages } from './router.js';
@@ -54,6 +156,7 @@ let messageLoopRunning = false;
let whatsapp: WhatsAppChannel;
const channels: Channel[] = [];
const queue = new GroupQueue();
const opencode = AGENT_BACKEND === 'opencode' ? new OpenCodeRuntime() : null;
function loadState(): void {
lastTimestamp = getRouterState('last_timestamp') || '';
@@ -221,6 +324,28 @@ async function runAgent(
chatJid: string,
onOutput?: (output: ContainerOutput) => Promise<void>,
): Promise<'success' | 'error'> {
// --- OpenCode backend ---
if (opencode) {
try {
const response = await opencode.chat(prompt, chatJid);
if (response.error) {
logger.error({ group: group.name, error: response.error }, 'OpenCode agent error');
if (onOutput) {
await onOutput({ status: 'error', error: response.error, result: null, newSessionId: response.sessionId });
}
return 'error';
}
if (onOutput && response.text) {
await onOutput({ status: 'success', result: response.text, error: null, newSessionId: response.sessionId });
}
return 'success';
} catch (err) {
logger.error({ group: group.name, err }, 'OpenCode agent error');
return 'error';
}
}
// --- Container backend (Claude Agent SDK) ---
const isMain = group.folder === MAIN_GROUP_FOLDER;
const sessionId = sessions[group.folder];
@@ -464,7 +589,11 @@ function ensureContainerSystemRunning(): void {
}
async function main(): Promise<void> {
ensureContainerSystemRunning();
if (AGENT_BACKEND === 'container') {
ensureContainerSystemRunning();
} else {
logger.info({ backend: AGENT_BACKEND }, 'Using OpenCode agent backend, skipping container system check');
}
initDatabase();
logger.info('Database initialized');
loadState();

View File

@@ -0,0 +1,31 @@
import { OpenCodeRuntime } from './runtime.js';
async function main() {
const runtime = new OpenCodeRuntime({
mode: 'cli',
timeoutMs: 30_000,
});
console.log('Status:', runtime.getStatus());
// Test 1: Empty message rejection
const empty = await runtime.chat(' ');
console.log('\nEmpty message test:', empty);
// Test 2: Actual chat (requires opencode binary installed)
try {
const response = await runtime.chat('Say hello in one sentence.', 'test-conv-1');
console.log('\nChat response:', response);
} catch (err) {
console.log('\nChat failed (opencode binary probably not installed):', err);
}
// Test 3: Session status after chat
console.log('\nStatus after chat:', runtime.getStatus());
// Cleanup
runtime.closeSession('test-conv-1');
console.log('\nSession closed. Final status:', runtime.getStatus());
}
main().catch(console.error);