Allow to change the agents name + Workspace files
Rolling release / release (push) Successful in 8m2s

This commit is contained in:
2026-09-12 15:30:11 -04:00
parent 874154f39a
commit 5317576087
30 changed files with 338 additions and 174 deletions
+72
View File
@@ -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;
}
+9 -6
View File
@@ -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
View File
@@ -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 });