feat: wire OpenCode as selectable agent backend via AGENT_BACKEND env var
Some checks failed
Update token count / update-tokens (push) Has been cancelled
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:
@@ -1,5 +1,5 @@
|
|||||||
<p align="center">
|
<p align="center">
|
||||||
<img src="assets/nanoclaw-logo.png" alt="Regolith" width="400">
|
<img src="assets/logo.jpeg" alt="Regolith" width="400">
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<p align="center">
|
<p align="center">
|
||||||
|
|||||||
BIN
assets/logo.jpeg
Normal file
BIN
assets/logo.jpeg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 63 KiB |
@@ -10,6 +10,7 @@ const envConfig = readEnvFile([
|
|||||||
'ASSISTANT_HAS_OWN_NUMBER',
|
'ASSISTANT_HAS_OWN_NUMBER',
|
||||||
'DISCORD_BOT_TOKEN',
|
'DISCORD_BOT_TOKEN',
|
||||||
'DISCORD_ONLY',
|
'DISCORD_ONLY',
|
||||||
|
'AGENT_BACKEND',
|
||||||
]);
|
]);
|
||||||
|
|
||||||
export const ASSISTANT_NAME =
|
export const ASSISTANT_NAME =
|
||||||
@@ -20,6 +21,11 @@ export const DISCORD_BOT_TOKEN =
|
|||||||
process.env.DISCORD_BOT_TOKEN || envConfig.DISCORD_BOT_TOKEN || '';
|
process.env.DISCORD_BOT_TOKEN || envConfig.DISCORD_BOT_TOKEN || '';
|
||||||
export const DISCORD_ONLY =
|
export const DISCORD_ONLY =
|
||||||
(process.env.DISCORD_ONLY || envConfig.DISCORD_ONLY) === 'true';
|
(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 POLL_INTERVAL = 2000;
|
||||||
export const SCHEDULER_POLL_INTERVAL = 60000;
|
export const SCHEDULER_POLL_INTERVAL = 60000;
|
||||||
|
|
||||||
|
|||||||
133
src/index.ts
133
src/index.ts
@@ -3,6 +3,7 @@ import fs from 'fs';
|
|||||||
import path from 'path';
|
import path from 'path';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
|
AGENT_BACKEND,
|
||||||
ASSISTANT_NAME,
|
ASSISTANT_NAME,
|
||||||
DATA_DIR,
|
DATA_DIR,
|
||||||
DISCORD_BOT_TOKEN,
|
DISCORD_BOT_TOKEN,
|
||||||
@@ -16,7 +17,107 @@ import { DiscordChannel } from './channels/discord.js';
|
|||||||
import { WhatsAppChannel } from './channels/whatsapp.js';
|
import { WhatsAppChannel } from './channels/whatsapp.js';
|
||||||
import {
|
import {
|
||||||
ContainerOutput,
|
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,
|
writeGroupsSnapshot,
|
||||||
writeTasksSnapshot,
|
writeTasksSnapshot,
|
||||||
} from './container-runner.js';
|
} from './container-runner.js';
|
||||||
@@ -41,6 +142,7 @@ import { findChannel, formatMessages, formatOutbound } from './router.js';
|
|||||||
import { startSchedulerLoop } from './task-scheduler.js';
|
import { startSchedulerLoop } from './task-scheduler.js';
|
||||||
import { Channel, NewMessage, RegisteredGroup } from './types.js';
|
import { Channel, NewMessage, RegisteredGroup } from './types.js';
|
||||||
import { logger } from './logger.js';
|
import { logger } from './logger.js';
|
||||||
|
import { OpenCodeRuntime } from './opencode/runtime.js';
|
||||||
|
|
||||||
// Re-export for backwards compatibility during refactor
|
// Re-export for backwards compatibility during refactor
|
||||||
export { escapeXml, formatMessages } from './router.js';
|
export { escapeXml, formatMessages } from './router.js';
|
||||||
@@ -54,6 +156,7 @@ let messageLoopRunning = false;
|
|||||||
let whatsapp: WhatsAppChannel;
|
let whatsapp: WhatsAppChannel;
|
||||||
const channels: Channel[] = [];
|
const channels: Channel[] = [];
|
||||||
const queue = new GroupQueue();
|
const queue = new GroupQueue();
|
||||||
|
const opencode = AGENT_BACKEND === 'opencode' ? new OpenCodeRuntime() : null;
|
||||||
|
|
||||||
function loadState(): void {
|
function loadState(): void {
|
||||||
lastTimestamp = getRouterState('last_timestamp') || '';
|
lastTimestamp = getRouterState('last_timestamp') || '';
|
||||||
@@ -221,6 +324,28 @@ async function runAgent(
|
|||||||
chatJid: string,
|
chatJid: string,
|
||||||
onOutput?: (output: ContainerOutput) => Promise<void>,
|
onOutput?: (output: ContainerOutput) => Promise<void>,
|
||||||
): Promise<'success' | 'error'> {
|
): 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 isMain = group.folder === MAIN_GROUP_FOLDER;
|
||||||
const sessionId = sessions[group.folder];
|
const sessionId = sessions[group.folder];
|
||||||
|
|
||||||
@@ -464,7 +589,11 @@ function ensureContainerSystemRunning(): void {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function main(): Promise<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();
|
initDatabase();
|
||||||
logger.info('Database initialized');
|
logger.info('Database initialized');
|
||||||
loadState();
|
loadState();
|
||||||
|
|||||||
31
src/opencode/test-opencode.ts
Normal file
31
src/opencode/test-opencode.ts
Normal 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);
|
||||||
Reference in New Issue
Block a user