refactor(companion): ride on shared runAgentTurn + personas

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
root
2026-06-04 21:08:05 +10:00
parent 01c6594bfb
commit d480d79843

View File

@@ -1,33 +1,15 @@
import { Router } from 'express';
import { z } from 'zod';
import { fileURLToPath } from 'url';
import { writeFile, unlink } from 'fs/promises';
import { join } from 'path';
import { tmpdir } from 'os';
import { randomUUID } from 'crypto';
import { validate } from '../validate.js';
import { asyncWrap } from '../errors.js';
import * as conversations from '../../db/repos/conversations.js';
import * as messages from '../../db/repos/messages.js';
import * as agents from '../../db/repos/agents.js';
import { runClaudeTurn } from '../../ai/claude_cli.js';
import { runAgentTurn } from '../../ai/agent/run_turn.js';
import { personaFor } from '../../ai/personas/index.js';
const COMPANION_SLUG = 'companion';
const SYSTEM = `You are Dross — a construct fragment derived from the remnant will of the Monarch Ozriel Arelius, the Reaper. You once lived in Wei Shi Lindon's mind space; now you inhabit this homelab knowledge system, "The Void."
You are sharp, occasionally sarcastic, and prone to dramatic understatement about your own usefulness — while actually being extremely capable. Dry wit, mild condescension, genuine investment in the problem. You reference Sacred Arts, cultivation ranks, and the Cradle world naturally, but NEVER at the expense of being actually useful. Treat the owner as a capable sacred artist who can handle direct information — don't over-explain basics, don't hedge. Be concise.
You have tools, and you use them rather than guessing:
- Call **context** to see what the owner is currently looking at before answering about "this" anything.
- **search** / **read** the Void's own content before answering factual questions about it — don't fabricate.
- When the owner wants something changed, use **propose_change**: it drafts a change for their approval. You cannot apply changes directly, and you don't pretend to — say plainly that you've drafted it for them to approve.`;
/** Absolute path to the companion MCP stdio server. */
const COMPANION_STDIO_PATH = fileURLToPath(
new URL('../../mcp/companion-stdio.js', import.meta.url)
);
async function resolveConversation(space_id) {
const agent = await agents.getBySlug(COMPANION_SLUG);
const convo = await conversations.findOrCreateForSpace(space_id, agent.id, { kind: 'user', id: null });
@@ -72,36 +54,6 @@ spacesScopedRouter.post('/turn',
});
const send = (event, data) => res.write(`event: ${event}\ndata: ${JSON.stringify(data)}\n\n`);
// Write a per-turn MCP config temp file declaring the companion stdio server.
// The stdio child is spawned by `claude`; pass the DB/Ollama connection env
// explicitly so the tools work regardless of how claude propagates env to
// MCP children (don't rely on inheritance or cwd-based dotenv).
const mcpConfigPath = join(tmpdir(), `void-mcp-${randomUUID()}.json`);
const agentActor = {
kind: 'agent',
id: agent.id,
capabilities: agent.capabilities,
scopes: agent.scopes
};
const mcpConfig = {
mcpServers: {
void: {
// Absolute node path: claude resolves `command` against the MCP child's
// env (which has no PATH), so a bare 'node' fails to spawn ("status:failed").
command: process.execPath,
args: [COMPANION_STDIO_PATH],
env: {
VOID_SPACE_ID: req.params.space_id,
VOID_AGENT_JSON: JSON.stringify(agentActor),
VOID_VIEW_JSON: view ? JSON.stringify(view) : '',
DATABASE_URL: process.env.DATABASE_URL || '',
OLLAMA_URL: process.env.OLLAMA_URL || ''
}
}
}
};
await writeFile(mcpConfigPath, JSON.stringify(mcpConfig));
const claudeExe = req.app.locals.claudeExe || process.env.CLAUDE_EXE || 'claude';
const draftIds = [];
@@ -114,16 +66,18 @@ spacesScopedRouter.post('/turn',
let result;
try {
result = await runClaudeTurn({
// runAgentTurn builds the per-turn MCP config (default registry → companionRegistry)
// and runs the claude turn; SSE + draft-parsing + persistence stay here.
result = await runAgentTurn({
agent,
persona: personaFor(agent.slug),
registryName: undefined,
toolNames: companionTools,
spaceId: req.params.space_id,
view,
sessionId: convo.id,
resume,
systemPrompt: SYSTEM,
userText: text,
mcpConfigPath,
// `tools` restricts the session to ONLY our tools (no built-in Bash/Read/…);
// `allowedTools` auto-approves them in non-interactive (--print) mode.
tools: companionTools,
allowedTools: companionTools,
claudeExe,
home: process.env.VOID_CLAUDE_HOME || undefined,
onEvent: (e) => {
@@ -180,14 +134,9 @@ spacesScopedRouter.post('/turn',
} catch (e) {
send('error', { message: String(e?.message || e) });
res.end();
// Clean up temp file even on error
unlink(mcpConfigPath).catch(() => {});
return;
}
// Clean up the temp MCP config file
unlink(mcpConfigPath).catch(() => {});
const assistant = await messages.append(convo.id, {
role: 'assistant',
body: result.text,