161 lines
4.8 KiB
JavaScript
161 lines
4.8 KiB
JavaScript
/**
|
|
* Standalone agent harness. Local QVAC inference, sandbox tools, JSONL sessions.
|
|
*/
|
|
|
|
const path = require('path');
|
|
const { EventEmitter } = require('events');
|
|
const loop = require('./agent/loop.js');
|
|
const sessions = require('./agent/sessions.js');
|
|
const sandbox = require('./agent/sandbox.js');
|
|
const customTools = require('./agent/custom-tools.js');
|
|
const mcp = require('./agent/mcp.js');
|
|
const engine = require('./lib/qvac.js');
|
|
const catalog = require('./lib/catalog.js');
|
|
|
|
function emitBridge(emitter) {
|
|
return function emit(_kind, payload) {
|
|
const ev = payload || {};
|
|
emitter.emit('event', ev);
|
|
if (ev.type) emitter.emit(ev.type, ev);
|
|
};
|
|
}
|
|
|
|
function wrapSession(summary, opts) {
|
|
const emitter = new EventEmitter();
|
|
const emit = emitBridge(emitter);
|
|
const sessionId = summary.id;
|
|
|
|
function on(type, fn) {
|
|
emitter.on(type, fn);
|
|
return () => emitter.off(type, fn);
|
|
}
|
|
|
|
async function prompt(text, payload) {
|
|
loop.markLive(sessionId);
|
|
try {
|
|
const session = sessions.load(sessionId);
|
|
const jobId = 'job_' + Date.now().toString(36);
|
|
const merged = Object.assign(
|
|
{
|
|
permissionMode: opts.permissionMode || 'ask',
|
|
webFetch: opts.webFetch === true,
|
|
system: opts.system,
|
|
maxTurns: opts.maxTurns,
|
|
maxShellCalls: opts.maxShellCalls,
|
|
maxToolRounds: opts.maxToolRounds,
|
|
voice: opts.voice,
|
|
},
|
|
payload || {}
|
|
);
|
|
return await loop.runTurn({
|
|
session,
|
|
userText: text,
|
|
emit,
|
|
jobId,
|
|
payload: merged,
|
|
});
|
|
} finally {
|
|
loop.finishLive(sessionId);
|
|
}
|
|
}
|
|
|
|
async function compact() {
|
|
const session = sessions.load(sessionId);
|
|
const ctxSize = (engine.getLoaded() && engine.getLoaded().ctxSize) || 8192;
|
|
const toolDefs = [];
|
|
session.history = await require('./agent/compaction.js').compactWithLlm(session.history, {
|
|
budgetTokens: require('./agent/compaction.js').historyBudget(ctxSize, toolDefs, 0),
|
|
tools: toolDefs,
|
|
complete: (o) => engine.complete(Object.assign({}, o, { desktopVision: false })),
|
|
});
|
|
sessions.replaceHistory(sessionId, session.history);
|
|
return session.history;
|
|
}
|
|
|
|
return {
|
|
id: sessionId,
|
|
cwd: summary.cwd,
|
|
model: summary.model,
|
|
on,
|
|
prompt,
|
|
compact,
|
|
permit(jobId, toolCallId, decision) {
|
|
const d = decision === true || decision === 'allow' || decision === 'always' ? decision : 'deny';
|
|
loop.resolvePermission(jobId, toolCallId, d === true ? 'allow' : d);
|
|
},
|
|
answer(toolCallId, choice) {
|
|
loop.resolveAsk(null, toolCallId, choice);
|
|
},
|
|
planDecision(decision) {
|
|
loop.resolvePlanDecision(sessionId, decision);
|
|
},
|
|
addTool(tool) {
|
|
customTools.register(sessionId, tool);
|
|
const session = sessions.load(sessionId);
|
|
session.customTools = customTools.list(sessionId);
|
|
sessions.saveSummary(session);
|
|
return session.customTools;
|
|
},
|
|
cancel() {
|
|
loop.cancel(sessionId);
|
|
},
|
|
async dispose() {
|
|
loop.forget(sessionId);
|
|
customTools.clear(sessionId);
|
|
sessions.remove(sessionId);
|
|
},
|
|
load() {
|
|
return sessions.load(sessionId);
|
|
},
|
|
};
|
|
}
|
|
|
|
async function create(opts) {
|
|
opts = opts || {};
|
|
const cwd = path.resolve(opts.cwd || process.cwd());
|
|
const extraRoots = Array.isArray(opts.roots) ? opts.roots.map((r) => path.resolve(r)) : [];
|
|
sandbox.setGrants({ roots: [cwd].concat(extraRoots) });
|
|
const model = opts.model || 'qwen3.5-4b';
|
|
const summary = sessions.create({
|
|
origin: opts.origin || 'local',
|
|
cwd,
|
|
workspace: cwd,
|
|
model,
|
|
title: opts.title || path.basename(cwd),
|
|
hostWorkspace: opts.hostWorkspace !== false,
|
|
builtinTools: opts.builtinTools === false ? false : Array.isArray(opts.builtinTools) ? opts.builtinTools : undefined,
|
|
goal: opts.goal || null,
|
|
planMode: opts.planMode,
|
|
sessionId: opts.sessionId,
|
|
});
|
|
customTools.setSession(summary.id, { hostWorkspace: summary.hostWorkspace });
|
|
if (opts.tools) {
|
|
customTools.register(summary.id, opts.tools);
|
|
summary.customTools = customTools.list(summary.id);
|
|
sessions.saveSummary(summary);
|
|
}
|
|
if (Array.isArray(opts.mcp)) {
|
|
for (const spec of opts.mcp) await mcp.register(spec, opts);
|
|
}
|
|
return wrapSession(summary, opts);
|
|
}
|
|
|
|
function load(sessionId, opts) {
|
|
const summary = sessions.load(sessionId);
|
|
if (summary.cwd) sandbox.setGrants({ roots: [path.resolve(summary.cwd)].concat(opts && opts.roots ? opts.roots : []) });
|
|
if (summary.customTools) customTools.register(summary.id, summary.customTools);
|
|
customTools.setSession(summary.id, { hostWorkspace: summary.hostWorkspace });
|
|
return wrapSession(summary, opts || {});
|
|
}
|
|
|
|
module.exports = {
|
|
create,
|
|
load,
|
|
list: () => sessions.list(),
|
|
catalog: () => catalog.listCatalog(),
|
|
suggest: (hw) => catalog.suggestProfile(hw),
|
|
engine,
|
|
mcp,
|
|
sandbox,
|
|
};
|