@@ -39,10 +39,12 @@ function copyIfMissing(from, to) {
|
||||
}
|
||||
|
||||
const OLD_AGENTS_BROWSER = '- `web_search` / `google_search` / `fetch_page` / `web_fetch` / `wiki_search` / `hn_search` / `code_search` — Playwright Chromium. For cookie walls or extra clicks, call `browser` with snapshot then click or type.';
|
||||
const NEW_AGENTS_BROWSER = `- Web tools share one headed Playwright Chromium window. \`web_search\` / \`google_search\` / \`wiki_search\` / \`hn_search\` / \`code_search\` find links. \`fetch_page\` / \`web_fetch\` read a public page. For cookie banners, forms, logins, or leftover challenges, call \`browser\`. Before a multi-step browse, \`read_file\` \`skills/browser/SKILL.md\`.
|
||||
const MID_AGENTS_BROWSER = `- Web tools share one headed Playwright Chromium window. \`web_search\` / \`google_search\` / \`wiki_search\` / \`hn_search\` / \`code_search\` find links. \`fetch_page\` / \`web_fetch\` read a public page. For cookie banners, forms, logins, or leftover challenges, call \`browser\`. Before a multi-step browse, \`read_file\` \`skills/browser/SKILL.md\`.
|
||||
- \`browser\` actions: \`navigate\` (needs \`url\`), \`snapshot\`, \`click\` (\`ref\` from the last snapshot), \`type\` (\`ref\` + \`text\`, optional \`submit\`), \`press\` (\`key\`), \`scroll\` (\`dy\`), \`wait\` (\`ms\`). Snapshot or navigate first. Refs change after every click. Do not use \`cu_observe\` or the shell for websites.`;
|
||||
const NEW_AGENTS_BROWSER = `- The only web tool is \`browser\`, the headed Jarvis Chromium window. \`web_search\`, \`web_fetch\`, and the other search tools are removed. Before a multi-step browse, \`read_file\` \`skills/browser/SKILL.md\`.
|
||||
- \`browser\` actions: \`navigate\` (needs \`url\`; returns page text), \`snapshot\`, \`click\` (\`ref\` from the last snapshot), \`type\` (\`ref\` + \`text\`, optional \`submit\`), \`press\` (\`key\`), \`scroll\` (\`dy\`), \`wait\` (\`ms\`). To search, navigate to a public search url, then click a result ref. Snapshot or navigate first. Refs change after every click. Do not use \`cu_observe\` or the shell for websites.`;
|
||||
const OLD_TOOLS_BROWSER = '- Public HTTP via curl or wget is blocked. Use `web_search` / `web_fetch` in the Jarvis Chromium window. For cookie walls, call `browser` with snapshot then click or type.';
|
||||
const NEW_TOOLS_BROWSER = `- Public HTTP via curl or wget is blocked. Use \`web_search\` / \`web_fetch\` in the Jarvis Chromium window.
|
||||
const MID_TOOLS_BROWSER = `- Public HTTP via curl or wget is blocked. Use \`web_search\` / \`web_fetch\` in the Jarvis Chromium window.
|
||||
|
||||
## Browser
|
||||
|
||||
@@ -52,6 +54,32 @@ const NEW_TOOLS_BROWSER = `- Public HTTP via curl or wget is blocked. Use \`web_
|
||||
- \`browser\` actions: \`navigate\` + \`url\`, \`snapshot\`, \`click\`/\`type\` with \`ref\` from the last snapshot, \`press\` + \`key\`, \`scroll\` + \`dy\`, \`wait\` + \`ms\`.
|
||||
- Snapshot or navigate before every click or type. Refs go stale after a click.
|
||||
- Do not use \`cu_observe\` or the shell for websites.`;
|
||||
const NEW_TOOLS_BROWSER = `- Public HTTP via curl or wget is blocked. Use \`browser\` in the Jarvis Chromium window.
|
||||
|
||||
## Browser
|
||||
|
||||
- \`browser\` is the only web tool. It drives one headed Playwright Chromium window.
|
||||
- \`web_search\`, \`google_search\`, \`fetch_page\`, \`web_fetch\`, \`wiki_search\`, \`hn_search\`, and \`code_search\` are removed. Do not call them.
|
||||
- To search, \`navigate\` to a public search url, then \`click\` a result ref.
|
||||
- \`navigate\` and \`snapshot\` return page \`text\`. Read it, then decide the next action.
|
||||
- Cookie walls, forms, leftover challenges: \`read_file\` \`skills/browser/SKILL.md\` for the playbook.
|
||||
- \`browser\` actions: \`navigate\` + \`url\`, \`snapshot\`, \`click\`/\`type\` with \`ref\` from the last snapshot, \`press\` + \`key\`, \`scroll\` + \`dy\`, \`wait\` + \`ms\`.
|
||||
- Snapshot or navigate before every click or type. Refs go stale after a click.
|
||||
- Do not use \`cu_observe\` or the shell for websites.`;
|
||||
|
||||
function rewriteBrowserSkill(dir) {
|
||||
const file = path.join(dir, 'skills/browser/SKILL.md');
|
||||
const template = path.join(TEMPLATE_DIR, 'skills/browser/SKILL.md');
|
||||
try {
|
||||
const text = fs.readFileSync(file, 'utf8');
|
||||
if (!/web_search|fetch_page|web_fetch/.test(text)) return false;
|
||||
fs.mkdirSync(path.dirname(file), { recursive: true });
|
||||
fs.writeFileSync(file, fs.readFileSync(template, 'utf8'));
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
function replaceOnce(file, from, to) {
|
||||
try {
|
||||
@@ -118,7 +146,10 @@ export function ensureAgentWorkspace({ name = 'Jarvis', prompt = '' } = {}) {
|
||||
copyIfMissing(path.join(TEMPLATE_DIR, rel), path.join(dest, rel));
|
||||
}
|
||||
replaceOnce(path.join(dest, 'AGENTS.md'), OLD_AGENTS_BROWSER, NEW_AGENTS_BROWSER);
|
||||
replaceOnce(path.join(dest, 'AGENTS.md'), MID_AGENTS_BROWSER, NEW_AGENTS_BROWSER);
|
||||
replaceOnce(path.join(dest, 'TOOLS.md'), OLD_TOOLS_BROWSER, NEW_TOOLS_BROWSER);
|
||||
replaceOnce(path.join(dest, 'TOOLS.md'), MID_TOOLS_BROWSER, NEW_TOOLS_BROWSER);
|
||||
rewriteBrowserSkill(dest);
|
||||
applyAssistantName(dest, name);
|
||||
applyAssistantPrompt(dest, prompt);
|
||||
return dest;
|
||||
|
||||
@@ -44,8 +44,8 @@ export class HarnessBridge extends EventEmitter {
|
||||
...createWebcamTools({ camera, capture: webcam, normalizer: webcamNormalizer }),
|
||||
...tools,
|
||||
],
|
||||
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', 'todo_write', 'task', 'update_goal', 'memory_search', 'memory_get', 'memory_write'],
|
||||
webFetch: true,
|
||||
builtinTools: ['read_file', 'write_file', 'search_replace', 'list_dir', 'grep', 'run_terminal_cmd', 'todo_write', 'task', 'update_goal', 'memory_search', 'memory_get', 'memory_write'],
|
||||
webFetch: false,
|
||||
browser,
|
||||
permissionMode,
|
||||
origin: 'jarvis-qvac',
|
||||
|
||||
+11
-4
@@ -10,11 +10,11 @@ import { CameraSession } from '../computer-use/camera-session.js';
|
||||
import { PortalCamera } from '../computer-use/portal-camera.js';
|
||||
import { VoiceStateMachine } from './voice-state.js';
|
||||
import { QvacScheduler } from './qvac-scheduler.js';
|
||||
import { cancelQvac, closeQvac, resumeQvac, suspendQvac, callQvac, cancelQvacRequest, qvacStatus } from './qvac-master.js';
|
||||
import { cancelQvac, closeQvac, resumeQvac, suspendQvac, callQvac, cancelQvacRequest, qvacStatus, applyAgentInference } from './qvac-master.js';
|
||||
import { PrivacyLog } from './privacy-log.js';
|
||||
import { VoiceLoop } from './voice-loop.js';
|
||||
import { QvacVoiceAdapter } from './voice-adapters.js';
|
||||
import { voiceSettings } from './voice-settings.js';
|
||||
import { voiceSettings, SETTINGS_FIELDS } from './voice-settings.js';
|
||||
import { createWakeEngine } from './wake-engine.js';
|
||||
import { DesktopObserver } from '../computer-use/observer.js';
|
||||
import { QvacPerception } from './perception.js';
|
||||
@@ -26,6 +26,7 @@ import { RuntimeTelemetry } from './telemetry.js';
|
||||
import { StateRecovery } from './recovery.js';
|
||||
import { spokenReply } from '../skills/voice-prompt.js';
|
||||
import { ensureAgentWorkspace, applyAssistantName, applyAssistantPrompt, normalizeAssistantName } from './agent-workspace.js';
|
||||
import { publicSettings } from '../apps/gnome-extension/[email protected]/settings-values.js';
|
||||
|
||||
function chunkText(ev) {
|
||||
if (ev == null) return '';
|
||||
@@ -38,6 +39,7 @@ export class JarvisDaemon extends EventEmitter {
|
||||
super();
|
||||
this.recovery = new StateRecovery(); const restored = this.recovery.load(); this.state = restored.state; this.mode = restored.mode;
|
||||
this.settings = voiceSettings();
|
||||
applyAgentInference(this.settings);
|
||||
this.startupSettings = this.settings;
|
||||
this.workspace = ensureAgentWorkspace({ name: this.settings.assistantName, prompt: this.settings.assistantPrompt });
|
||||
this.voice = new VoiceStateMachine({ idleMs: this.settings.idleMinutes * 60_000 });
|
||||
@@ -302,6 +304,11 @@ export class JarvisDaemon extends EventEmitter {
|
||||
await this.voiceLoop?.stop?.();
|
||||
this.voiceLoop = null;
|
||||
this.settings = next;
|
||||
const inference = applyAgentInference(next);
|
||||
await inference.release;
|
||||
if (next.agentInference !== previous.agentInference || next.groqModel !== previous.groqModel) {
|
||||
await this.harness.resetContext();
|
||||
}
|
||||
this.voice.idleMs = next.idleMinutes * 60_000;
|
||||
const who = applyAssistantName(this.workspace, next.assistantName);
|
||||
applyAssistantPrompt(this.workspace, next.assistantPrompt);
|
||||
@@ -318,7 +325,7 @@ export class JarvisDaemon extends EventEmitter {
|
||||
Object.assign(this.webcamNormalizer, { maxLongEdge: next.webcamMaxEdge, quality: next.screenshotQuality });
|
||||
await this.startVoice();
|
||||
this.voice.cancel(); this.setState('ARMED');
|
||||
return JSON.stringify({ applied: true, voice: this.voiceLoop.status, environmentOverrides: ['JARVIS_TTS_MODEL', 'JARVIS_ASR_MODEL', 'JARVIS_WAKE_COMMAND', 'JARVIS_QVAC_MODEL'].filter(key => process.env[key]), restartRequired: ['chatModel', 'modelProfile', 'maxTurns', 'maxShellCalls', 'maxToolRounds', 'fsAccess'].filter(key => next[key] !== this.startupSettings[key]) });
|
||||
return JSON.stringify({ applied: true, voice: this.voiceLoop.status, environmentOverrides: ['JARVIS_TTS_MODEL', 'JARVIS_ASR_MODEL', 'JARVIS_WAKE_COMMAND', 'JARVIS_QVAC_MODEL', 'GROQ_API_KEY', 'JARVIS_GROQ_API_KEY'].filter(key => process.env[key]), restartRequired: ['chatModel', 'modelProfile', 'maxTurns', 'maxShellCalls', 'maxToolRounds', 'fsAccess'].filter(key => next[key] !== this.startupSettings[key]) });
|
||||
}
|
||||
async previewVoice(text) {
|
||||
if (this.locked) throw new Error('Unlock the desktop to preview a voice');
|
||||
@@ -365,7 +372,7 @@ export class JarvisDaemon extends EventEmitter {
|
||||
this.setState('ARMED');
|
||||
}
|
||||
}
|
||||
runtimeStatus() { return JSON.stringify({ local: true, qvac: qvacStatus(), scheduler: this.scheduler.status(), scheduler_metrics: this.scheduler.metrics(), telemetry: this.telemetry.snapshot(), settings: this.settings, computer: this.computer.status(), camera: this.camera.status(), voice: this.voiceLoop ? { ...this.voiceLoop.metrics.snapshot(), ...this.voiceLoop.status, muted: this.muted } : { muted: this.muted }, muted: this.muted, p2p: { enabled: process.env.JARVIS_P2P_ENABLE === '1', inference: false, memorySync: false } }); }
|
||||
runtimeStatus() { return JSON.stringify({ local: !this.settings.agentInference || this.settings.agentInference === 'local', agentInference: this.settings.agentInference || 'local', qvac: qvacStatus(), scheduler: this.scheduler.status(), scheduler_metrics: this.scheduler.metrics(), telemetry: this.telemetry.snapshot(), settings: publicSettings(this.settings, SETTINGS_FIELDS), computer: this.computer.status(), camera: this.camera.status(), voice: this.voiceLoop ? { ...this.voiceLoop.metrics.snapshot(), ...this.voiceLoop.status, muted: this.muted } : { muted: this.muted }, muted: this.muted, p2p: { enabled: process.env.JARVIS_P2P_ENABLE === '1', inference: false, memorySync: false } }); }
|
||||
async assessModelFit(model) { return JSON.stringify(await callQvac('assessModelFit', { modelSrc: String(model) })); }
|
||||
async downloadModel(model) { return JSON.stringify(await callQvac('downloadAsset', { modelSrc: String(model) })); }
|
||||
async cancelModel(model) { return JSON.stringify(await cancelQvacRequest({ modelId: String(model) })); }
|
||||
|
||||
+25
-2
@@ -48,7 +48,11 @@ export function assertSdkVersion() {
|
||||
}
|
||||
|
||||
export async function acquireQvac({ auxiliaryOnly = false } = {}) {
|
||||
if (auxiliaryOnly) { await qvacSdk(); ownerCount += 1; return; }
|
||||
if (auxiliaryOnly) {
|
||||
await qvacSdk();
|
||||
ownerCount += 1;
|
||||
return Agent.engine.getLoaded?.();
|
||||
}
|
||||
if (!loadPromise) {
|
||||
// Publish the promise before the first await so concurrent callers share
|
||||
// resource discovery and loading. Count only successful acquisitions.
|
||||
@@ -71,6 +75,18 @@ export async function acquireQvac({ auxiliaryOnly = false } = {}) {
|
||||
return loaded;
|
||||
}
|
||||
|
||||
export function applyAgentInference(settings = voiceSettings()) {
|
||||
const provider = settings.agentInference === 'groq' ? 'groq' : 'local';
|
||||
const apiKey = String(process.env.GROQ_API_KEY || process.env.JARVIS_GROQ_API_KEY || settings.groqApiKey || '').trim();
|
||||
const model = String(settings.groqModel || 'openai/gpt-oss-20b').trim();
|
||||
if (typeof Agent.engine.configureRemote === 'function') {
|
||||
Agent.engine.configureRemote({ provider, apiKey, model });
|
||||
}
|
||||
// Groq only thinks about Chrome page results. The local chat model stays
|
||||
// loaded so files, shell, desktop, and camera still run on this computer.
|
||||
return { provider, model, keySet: Boolean(apiKey), release: Promise.resolve() };
|
||||
}
|
||||
|
||||
export async function qvacSdk() {
|
||||
return Agent.engine.ensureInit();
|
||||
}
|
||||
@@ -200,7 +216,14 @@ export async function callQvac(method, input) {
|
||||
}
|
||||
|
||||
export function qvacStatus() {
|
||||
return { ...QVAC_MASTER, owners: ownerCount, loaded: Agent.engine.getLoaded(), auxiliaryModels: auxiliaryModels.size };
|
||||
const loaded = Agent.engine.getLoaded();
|
||||
return {
|
||||
...QVAC_MASTER,
|
||||
owners: ownerCount,
|
||||
loaded,
|
||||
auxiliaryModels: auxiliaryModels.size,
|
||||
agentInference: Agent.engine.remoteActive?.() ? 'groq' : 'local',
|
||||
};
|
||||
}
|
||||
|
||||
export { Agent };
|
||||
|
||||
Reference in New Issue
Block a user