Allow to change the agents name + Workspace files
Rolling release / release (push) Successful in 8m2s
Rolling release / release (push) Successful in 8m2s
This commit is contained in:
@@ -0,0 +1,72 @@
|
||||
import fs from 'node:fs';
|
||||
import os from 'node:os';
|
||||
import path from 'node:path';
|
||||
|
||||
const TEMPLATE_DIR = path.resolve(new URL('../vendor/agent-harness/agent-workspace', import.meta.url).pathname);
|
||||
const SEED_FILES = [
|
||||
'SOUL.md',
|
||||
'IDENTITY.md',
|
||||
'AGENTS.md',
|
||||
'USER.md',
|
||||
'MEMORY.md',
|
||||
'BOOTSTRAP.md',
|
||||
'HEARTBEAT.md',
|
||||
'TOOLS.md',
|
||||
'skills/skill-creator/SKILL.md',
|
||||
];
|
||||
|
||||
export function normalizeAssistantName(value) {
|
||||
const text = String(value || '').replace(/[\r\n\t]/g, ' ').trim().replace(/\s+/g, ' ').slice(0, 32);
|
||||
if (!text || /[<>&]/.test(text)) return 'Jarvis';
|
||||
return text;
|
||||
}
|
||||
|
||||
export function agentWorkspaceDir() {
|
||||
return path.join(process.env.XDG_DATA_HOME || path.join(os.homedir(), '.local/share'), 'jarvis/workspace');
|
||||
}
|
||||
|
||||
export function templateWorkspaceDir() {
|
||||
return TEMPLATE_DIR;
|
||||
}
|
||||
|
||||
function copyIfMissing(from, to) {
|
||||
if (fs.existsSync(to)) return false;
|
||||
fs.mkdirSync(path.dirname(to), { recursive: true });
|
||||
fs.copyFileSync(from, to);
|
||||
return true;
|
||||
}
|
||||
|
||||
export function applyAssistantName(dir, name) {
|
||||
const who = normalizeAssistantName(name);
|
||||
const identity = path.join(dir, 'IDENTITY.md');
|
||||
let text = '';
|
||||
try { text = fs.readFileSync(identity, 'utf8'); } catch { text = '# IDENTITY.md\n\n'; }
|
||||
if (/\*\*Name:\*\*/.test(text)) text = text.replace(/\*\*Name:\*\*.*/, `**Name:** ${who}`);
|
||||
else text += `\n- **Name:** ${who}\n`;
|
||||
fs.mkdirSync(dir, { recursive: true });
|
||||
fs.writeFileSync(identity, text);
|
||||
const soul = path.join(dir, 'SOUL.md');
|
||||
try {
|
||||
let soulText = fs.readFileSync(soul, 'utf8');
|
||||
soulText = soulText.replace(/^# SOUL\.md:.*/m, `# SOUL.md: ${who}`);
|
||||
fs.writeFileSync(soul, soulText);
|
||||
} catch {}
|
||||
const memory = path.join(dir, 'MEMORY.md');
|
||||
try {
|
||||
let mem = fs.readFileSync(memory, 'utf8');
|
||||
if (/Assistant name:/.test(mem)) mem = mem.replace(/Assistant name:.*/, `Assistant name: ${who}`);
|
||||
fs.writeFileSync(memory, mem);
|
||||
} catch {}
|
||||
return who;
|
||||
}
|
||||
|
||||
export function ensureAgentWorkspace({ name = 'Jarvis' } = {}) {
|
||||
const dest = agentWorkspaceDir();
|
||||
fs.mkdirSync(path.join(dest, 'memory'), { recursive: true });
|
||||
fs.mkdirSync(path.join(dest, 'skills'), { recursive: true });
|
||||
for (const rel of SEED_FILES) {
|
||||
copyIfMissing(path.join(TEMPLATE_DIR, rel), path.join(dest, rel));
|
||||
}
|
||||
applyAssistantName(dest, name);
|
||||
return dest;
|
||||
}
|
||||
@@ -5,10 +5,11 @@ import { acquireQvac, closeQvac, releaseQvac, Agent, QVAC_MASTER } from './qvac-
|
||||
import { createRuntimeTools } from '../skills/runtime-tools.js';
|
||||
import { createPhase2Tools, filesystemRoots } from '../skills/phase2-tools.js';
|
||||
import { createQvacTools } from '../skills/qvac-tools.js';
|
||||
import { VOICE_SYSTEM_PROMPT, parseHudSidecar } from '../skills/voice-prompt.js';
|
||||
import { voiceSystemPrompt, parseHudSidecar } from '../skills/voice-prompt.js';
|
||||
import { createComputerObserveTools } from '../skills/computer-observe.js';
|
||||
import { createComputerActTools } from '../skills/computer-act.js';
|
||||
import { createPhase9GatewayTool } from '../skills/phase9-tools.js';
|
||||
import { ensureAgentWorkspace, normalizeAssistantName } from './agent-workspace.js';
|
||||
|
||||
export function harnessRoots(fsAccess) {
|
||||
if (fsAccess === 'filesystem') return ['/'];
|
||||
@@ -17,29 +18,31 @@ export function harnessRoots(fsAccess) {
|
||||
}
|
||||
|
||||
export class HarnessBridge extends EventEmitter {
|
||||
constructor({ cwd = process.cwd(), model = QVAC_MASTER.model, tools = [], computer, observer, actuator, permissionMode = 'ask', fsAccess } = {}) {
|
||||
constructor({ cwd, model = QVAC_MASTER.model, tools = [], computer, observer, actuator, permissionMode = 'ask', fsAccess } = {}) {
|
||||
super();
|
||||
const settings = voiceSettings();
|
||||
const access = fsAccess ?? settings.fsAccess;
|
||||
const assistantName = normalizeAssistantName(settings.assistantName);
|
||||
const workspace = cwd || ensureAgentWorkspace({ name: assistantName });
|
||||
const roots = harnessRoots(access);
|
||||
this.options = {
|
||||
cwd,
|
||||
cwd: workspace,
|
||||
roots,
|
||||
model,
|
||||
tools: [
|
||||
...createRuntimeTools({ computer }),
|
||||
...createPhase2Tools({ cwd, computer, roots: filesystemRoots(access, cwd) }),
|
||||
...createPhase2Tools({ cwd: workspace, computer, roots: filesystemRoots(access, workspace) }),
|
||||
...createComputerObserveTools({ computer, observer }),
|
||||
...createComputerActTools({ actuator }),
|
||||
...createQvacTools(),
|
||||
...createPhase9GatewayTool(),
|
||||
...tools,
|
||||
],
|
||||
builtinTools: ['read_file', 'list_dir', 'grep', 'run_terminal_cmd', 'web_fetch', 'fetch_page', 'google_search', 'web_search', 'wiki_search', 'hn_search', 'code_search'],
|
||||
builtinTools: ['read_file', 'write_file', 'search_replace', 'list_dir', 'grep', 'run_terminal_cmd', 'web_fetch', 'fetch_page', 'google_search', 'web_search', 'wiki_search', 'hn_search', 'code_search'],
|
||||
webFetch: true,
|
||||
permissionMode,
|
||||
origin: 'jarvis-qvac',
|
||||
system: VOICE_SYSTEM_PROMPT,
|
||||
system: voiceSystemPrompt(assistantName),
|
||||
voice: true,
|
||||
maxTurns: settings.maxTurns,
|
||||
maxShellCalls: settings.maxShellCalls,
|
||||
|
||||
+9
-2
@@ -21,7 +21,8 @@ import { PortalInputBackend } from '../computer-use/portal-input.js';
|
||||
import { ComputerActuator } from '../computer-use/actuator.js';
|
||||
import { RuntimeTelemetry } from './telemetry.js';
|
||||
import { StateRecovery } from './recovery.js';
|
||||
import { spokenReply } from '../skills/voice-prompt.js';
|
||||
import { spokenReply, voiceSystemPrompt } from '../skills/voice-prompt.js';
|
||||
import { ensureAgentWorkspace, applyAssistantName, normalizeAssistantName } from './agent-workspace.js';
|
||||
|
||||
export class JarvisDaemon extends EventEmitter {
|
||||
constructor() {
|
||||
@@ -29,6 +30,7 @@ export class JarvisDaemon extends EventEmitter {
|
||||
this.recovery = new StateRecovery(); const restored = this.recovery.load(); this.state = restored.state; this.mode = restored.mode;
|
||||
this.settings = voiceSettings();
|
||||
this.startupSettings = this.settings;
|
||||
this.workspace = ensureAgentWorkspace({ name: this.settings.assistantName });
|
||||
this.voice = new VoiceStateMachine({ idleMs: this.settings.idleMinutes * 60_000 });
|
||||
this.scheduler = new QvacScheduler({ concurrency: 1 });
|
||||
this.audit = new ComputerAudit();
|
||||
@@ -41,7 +43,7 @@ export class JarvisDaemon extends EventEmitter {
|
||||
framebuffer: { capture: (output) => this.input.captureFrame(output) },
|
||||
});
|
||||
this.actuator = new ComputerActuator({ session: this.computer, input: this.input, find: ({ ref }) => this.observer.lastTree.filter((node) => node.ref === ref), atspiAction: (target, action) => this.observer.atspi.action(target, action), highlight: async (target, action) => this.emit('ComputerHighlight', JSON.stringify({ rect: target?.rect || null, label: `${action} ${target?.name || ''}` })) , audit: this.audit });
|
||||
this.harness = new HarnessBridge({ cwd: process.cwd(), computer: this.computer, observer: this.observer, actuator: this.actuator, fsAccess: this.settings.fsAccess });
|
||||
this.harness = new HarnessBridge({ cwd: this.workspace, computer: this.computer, observer: this.observer, actuator: this.actuator, fsAccess: this.settings.fsAccess });
|
||||
this.log = new PrivacyLog();
|
||||
this.locked = false;
|
||||
this.lastReply = '';
|
||||
@@ -234,6 +236,11 @@ export class JarvisDaemon extends EventEmitter {
|
||||
this.voiceLoop = null;
|
||||
this.settings = next;
|
||||
this.voice.idleMs = next.idleMinutes * 60_000;
|
||||
const who = applyAssistantName(this.workspace, next.assistantName);
|
||||
if (who !== normalizeAssistantName(previous.assistantName)) {
|
||||
this.harness.options.system = voiceSystemPrompt(who);
|
||||
await this.harness.resetContext();
|
||||
}
|
||||
if (['computerMode', 'computerSteps', 'computerGrantMinutes'].some(key => next[key] !== previous[key])) this.computerRevoke();
|
||||
Object.assign(this.computer, { mode: next.computerMode, stepsMax: next.computerSteps, grantMinutes: next.computerGrantMinutes });
|
||||
Object.assign(this.observer.normalizer, { maxLongEdge: next.screenshotMaxEdge, quality: next.screenshotQuality });
|
||||
|
||||
Reference in New Issue
Block a user