feat(companion): Dross persona (Cradle) + migration 008 rename; remove dead API-key path
- system prompt = Dross (Ozriel's construct fragment, per Void 1.0), with tool guidance - migration 008 renames the seeded agent 'companion' → display name 'Dross' - removed lib/ai/anthropic.js + lib/ai/agent/runtime.js + tests + @anthropic-ai/sdk dep (companion now runs via the claude CLI; kept lib/ai/secret.js for the Vaultwarden roadmap) Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -1,58 +0,0 @@
|
||||
// Tool-use loop. callModel + registry injected (no network here).
|
||||
// Emits: { type:'tool', tool, args, status } | { type:'delta', text }
|
||||
// | { type:'draft', pending_change_id, summary }
|
||||
// Returns: { text, toolTrace, draftIds, usage, stoppedOnGuard }
|
||||
export async function runTurn({ callModel, registry, system, messages, ctx, onEvent, maxIterations = 6 }) {
|
||||
const convo = [...messages];
|
||||
const toolTrace = [];
|
||||
const draftIds = [];
|
||||
let usage = {};
|
||||
let stoppedOnGuard = false;
|
||||
|
||||
for (let i = 0; i < maxIterations; i++) {
|
||||
const res = await callModel({
|
||||
system,
|
||||
messages: convo,
|
||||
tools: registry.toAnthropicTools(),
|
||||
onTextDelta: (t) => onEvent?.({ type: 'delta', text: t })
|
||||
});
|
||||
usage = res.usage || usage;
|
||||
|
||||
if (!res.toolUses?.length) {
|
||||
return { text: res.text, toolTrace, draftIds, usage, stoppedOnGuard };
|
||||
}
|
||||
|
||||
// Record the assistant turn (text + tool_use blocks) for the next round.
|
||||
convo.push({
|
||||
role: 'assistant',
|
||||
content: [
|
||||
...(res.text ? [{ type: 'text', text: res.text }] : []),
|
||||
...res.toolUses.map(t => ({ type: 'tool_use', id: t.id, name: t.name, input: t.input }))
|
||||
]
|
||||
});
|
||||
|
||||
const toolResults = [];
|
||||
for (const call of res.toolUses) {
|
||||
const tool = registry.getTool(call.name);
|
||||
let result;
|
||||
try {
|
||||
result = tool ? await tool.handler(call.input, ctx) : { error: `unknown tool ${call.name}` };
|
||||
} catch (e) {
|
||||
result = { error: String(e?.message || e) };
|
||||
}
|
||||
const status = result?.error ? 'error' : 'done';
|
||||
toolTrace.push({ tool: call.name, args: call.input, ok: !result?.error });
|
||||
onEvent?.({ type: 'tool', tool: call.name, args: call.input, status });
|
||||
if (result?.pending_change_id) {
|
||||
draftIds.push(result.pending_change_id);
|
||||
onEvent?.({ type: 'draft', pending_change_id: result.pending_change_id, summary: result.summary });
|
||||
}
|
||||
toolResults.push({ type: 'tool_result', tool_use_id: call.id, content: JSON.stringify(result) });
|
||||
}
|
||||
convo.push({ role: 'user', content: toolResults });
|
||||
|
||||
if (i === maxIterations - 1) stoppedOnGuard = true;
|
||||
}
|
||||
|
||||
return { text: '', toolTrace, draftIds, usage, stoppedOnGuard };
|
||||
}
|
||||
@@ -1,80 +0,0 @@
|
||||
import Anthropic from '@anthropic-ai/sdk';
|
||||
import { resolveSecret } from './secret.js';
|
||||
|
||||
export const DEFAULT_MODEL = process.env.ANTHROPIC_MODEL || 'claude-sonnet-4-6';
|
||||
|
||||
/**
|
||||
* Build an Anthropic client, resolving the API key via the secret resolver.
|
||||
* Key reference can be overridden with ANTHROPIC_API_KEY_REF env var.
|
||||
*/
|
||||
export function getAnthropicClient() {
|
||||
const key = resolveSecret(process.env.ANTHROPIC_API_KEY_REF || 'env:ANTHROPIC_API_KEY');
|
||||
if (!key) {
|
||||
throw new Error(
|
||||
'Anthropic API key not configured (set ANTHROPIC_API_KEY or ANTHROPIC_API_KEY_REF)'
|
||||
);
|
||||
}
|
||||
return new Anthropic({ apiKey: key });
|
||||
}
|
||||
|
||||
/**
|
||||
* Factory that returns a callModel function bound to a specific client + model.
|
||||
*
|
||||
* @param {object} opts
|
||||
* @param {object} opts.client - Anthropic client (or fake for tests)
|
||||
* @param {string} [opts.model] - Model ID (default: DEFAULT_MODEL)
|
||||
* @param {number} [opts.maxTokens] - max_tokens (default: 1024)
|
||||
* @returns {Function} callModel({ system, messages, tools, onTextDelta })
|
||||
*
|
||||
* callModel returns a stable shape:
|
||||
* { text: string, toolUses: [{id, name, input}], stopReason: string, usage: object }
|
||||
*
|
||||
* Text deltas are streamed via onTextDelta(chunk) as they arrive.
|
||||
* Tool-use blocks are collected from the final assembled message.
|
||||
*/
|
||||
export function makeCallModel({ client, model = DEFAULT_MODEL, maxTokens = 1024 }) {
|
||||
return async function callModel({ system, messages, tools, onTextDelta }) {
|
||||
// client.messages.stream returns a MessageStream: AsyncIterable<MessageStreamEvent>
|
||||
// with a finalMessage() method that resolves to the assembled Message.
|
||||
const stream = client.messages.stream({
|
||||
model,
|
||||
max_tokens: maxTokens,
|
||||
...(system !== undefined && { system }),
|
||||
messages,
|
||||
...(tools !== undefined && tools !== null && { tools }),
|
||||
});
|
||||
|
||||
// Iterate events; fire onTextDelta for text_delta events only.
|
||||
// RawContentBlockDeltaEvent: { type: 'content_block_delta', index, delta }
|
||||
// TextDelta: { type: 'text_delta', text }
|
||||
for await (const event of stream) {
|
||||
if (
|
||||
event.type === 'content_block_delta' &&
|
||||
event.delta?.type === 'text_delta'
|
||||
) {
|
||||
onTextDelta?.(event.delta.text);
|
||||
}
|
||||
}
|
||||
|
||||
// finalMessage() resolves to the fully assembled Message object.
|
||||
const final = await stream.finalMessage();
|
||||
|
||||
// Collect text from all text blocks (typically just one, but join defensively).
|
||||
const text = final.content
|
||||
.filter((b) => b.type === 'text')
|
||||
.map((b) => b.text)
|
||||
.join('');
|
||||
|
||||
// Collect tool_use blocks, normalising to a stable {id, name, input} shape.
|
||||
const toolUses = final.content
|
||||
.filter((b) => b.type === 'tool_use')
|
||||
.map((b) => ({ id: b.id, name: b.name, input: b.input }));
|
||||
|
||||
return {
|
||||
text,
|
||||
toolUses,
|
||||
stopReason: final.stop_reason,
|
||||
usage: final.usage,
|
||||
};
|
||||
};
|
||||
}
|
||||
@@ -14,9 +14,14 @@ import { runClaudeTurn } from '../../ai/claude_cli.js';
|
||||
|
||||
const COMPANION_SLUG = 'companion';
|
||||
|
||||
const SYSTEM = `You are the Void companion — a concise, helpful assistant embedded in a personal knowledge system.
|
||||
Ground answers in the Void's content: call the context tool to see what the owner is looking at, and search/read before answering factual questions.
|
||||
When the owner asks you to change something, use propose_change — it creates a draft they approve; you cannot apply changes directly. Be brief.`;
|
||||
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(
|
||||
|
||||
6
lib/db/migrations/008_dross.sql
Normal file
6
lib/db/migrations/008_dross.sql
Normal file
@@ -0,0 +1,6 @@
|
||||
-- lib/db/migrations/008_dross.sql
|
||||
-- Plan 5b: the companion's persona is Dross (Ozriel's construct fragment from
|
||||
-- Cradle — the original from Void 1.0). slug stays 'companion' (route key);
|
||||
-- only the display name changes (shown in the rail header).
|
||||
|
||||
UPDATE agents SET name = 'Dross' WHERE slug = 'companion';
|
||||
Reference in New Issue
Block a user