v1.1.0: composer advisor picker (model + effort submenu, off/same-as-agent), live settings route, tests

This commit is contained in:
Agent committed 2026-10-11 00:42:38 +00:00
1 parent a47bcb03ab
commit 22dcc3a29c
4 files changed
+417 -12

No files matched your search

+65 -6
View File
@@ -10,11 +10,26 @@
* still-unacknowledged blocker steers the agent into one more step.
* Reviews run in the background and never delay the main loop unless `blocking: true`.
*/
import fs from 'node:fs'
import os from 'node:os'
import path from 'node:path'
import { createUserMessage } from '@deepseek-ai/dsh-llm'
export const name = 'omp-advisor'
export const inject = ['agents', 'llm']
/** Live settings chosen in the UI (enabled + advisor route), persisted next to the profile. */
function settingsFile() {
const dir = process.env.DSH_PROFILE_DIR || path.join(process.env.DSH_HOME || path.join(os.homedir(), '.dsh'), 'profiles', 'web')
return path.join(dir, 'omp-advisor.json')
}
function loadSettings() { try { return JSON.parse(fs.readFileSync(settingsFile(), 'utf8')) } catch { return {} } }
function saveSettings(v) {
const f = settingsFile()
fs.mkdirSync(path.dirname(f), { recursive: true })
fs.writeFileSync(f, JSON.stringify(v, null, 2))
}
const SOURCE = { kind: 'omp-advisor' }
const SYSTEM = `You are the ADVISOR: a senior engineer silently pairing with a coding agent. You see a digest of the conversation and the agent's latest step (its message, tool calls and results). Intervene only when it matters:
@@ -40,11 +55,18 @@ export function apply(ctx, config = {}) {
...config,
}
const LEVEL = { ok: 0, note: 1, concern: 2, blocker: 3 }
const minLevel = LEVEL[cfg.minLevel] ?? 1
let live = loadSettings() // { enabled?, provider?, model?, reasoningEffort? }
const eff = () => ({
enabled: live.enabled ?? cfg.enabled,
provider: live.provider ?? cfg.provider,
model: live.model ?? cfg.model,
reasoningEffort: live.reasoningEffort === null ? undefined : (live.reasoningEffort ?? cfg.reasoningEffort),
})
const minLevel = () => LEVEL[live.minLevel ?? cfg.minLevel] ?? 1
const installed = new Map()
function install(agent) {
if (installed.has(agent) || !agent?.ctx || !cfg.enabled) return
if (installed.has(agent) || !agent?.ctx) return
if (cfg.skipSubagents && agent.session?.header?.parentSession) return
const st = { pending: null, queue: [], history: [], step: 0, openBlocker: null, lastReviewedSeq: -1, failures: 0 }
const disposers = []
@@ -53,6 +75,7 @@ export function apply(ctx, config = {}) {
disposers.push(agent.ctx.on('agent/pre-step', async (payload, next) => {
const decision = await next()
if (decision.kind !== 'enter') return decision
if (!eff().enabled) { st.queue.length = 0; return decision }
st.step++
if (payload.step > 1 && st.step % cfg.everyNSteps === 0 && !st.pending) {
st.pending = review(agent, st, payload.signal).finally(() => { st.pending = null })
@@ -66,6 +89,7 @@ export function apply(ctx, config = {}) {
// At the end of a turn: deliver late advice; a blocker forces one more step.
disposers.push(agent.ctx.on('agent/turn-stopping', async ({ signal }) => {
if (!eff().enabled) return
if (st.pending) {
let t
await Promise.race([st.pending, new Promise((r) => { t = setTimeout(r, cfg.blocking ? cfg.timeoutMs : 4000) })]).finally(() => clearTimeout(t))
@@ -90,8 +114,11 @@ export function apply(ctx, config = {}) {
async function review(agent, st, signal) {
try {
const header = agent.session.requestHeader?.()
const provider = cfg.provider || header?.config?.provider
const model = cfg.model || header?.config?.model
const e = eff()
// A half-set route (provider without model) falls back to the agent's model as a whole.
const own = e.provider && e.model
const provider = own ? e.provider : header?.config?.provider
const model = own ? e.model : header?.config?.model
if (!provider || !model) return
const digest = buildDigest(agent.session, st)
if (!digest) return
@@ -103,7 +130,7 @@ export function apply(ctx, config = {}) {
try {
for await (const ch of ctx.llm.stream({
provider, model,
...(cfg.reasoningEffort ? { reasoningEffort: cfg.reasoningEffort } : {}),
...(own && e.reasoningEffort ? { reasoningEffort: e.reasoningEffort } : {}),
system: SYSTEM,
messages: [createUserMessage({ content: [{ type: 'text', text: `PREVIOUS ADVICE:\n${prev}\n\nTRANSCRIPT DIGEST (oldest first, latest step last):\n${digest}\n\nYour JSON verdict:` }], source: SOURCE })],
maxTokens: 600,
@@ -116,7 +143,7 @@ export function apply(ctx, config = {}) {
const v = parseVerdict(text)
if (!v) return
st.history.push(v)
if ((LEVEL[v.verdict] ?? 0) >= minLevel && LEVEL[v.verdict] > 0) {
if ((LEVEL[v.verdict] ?? 0) >= minLevel() && LEVEL[v.verdict] > 0) {
if (st.history.slice(0, -1).some((h) => h.message === v.message)) return
st.queue.push(v)
}
@@ -163,6 +190,38 @@ export function apply(ctx, config = {}) {
const contentText = (c) => (Array.isArray(c) ? c.map((x) => (x.type === 'text' ? x.text : `[${x.type}]`)).join('\n') : String(c ?? ''))
const clip = (s, n) => { const t = String(s ?? ''); return t.length > n ? `${t.slice(0, n)}…[+${t.length - n} chars]` : t }
// ---------------------------------------------------------------- Web UI settings routes
// Mounted only while the web server and its trust fence exist (a headless profile has neither).
ctx.inject?.(['webServer', 'connection'], (wctx) => {
const webServer = wctx.webServer
const connection = wctx.connection
const send = (res, code, body) => { res.statusCode = code; res.setHeader('content-type', 'application/json; charset=utf-8'); res.setHeader('cache-control', 'no-store'); res.end(JSON.stringify(body)) }
const readBody = async (req) => { let b = ''; for await (const c of req) { b += c; if (b.length > 16384) throw new Error('body too large') } return b ? JSON.parse(b) : {} }
const state = () => ({ ...eff(), configured: { provider: cfg.provider || null, model: cfg.model || null }, minLevel: live.minLevel ?? cfg.minLevel })
wctx.effect(() => webServer.register({
kind: 'exact',
path: '/omp-advisor/settings',
handler: async (req, res) => {
const rej = connection.requestRejection(req)
if (rej !== undefined) return send(res, rej, { code: 'rejected' })
try {
if (req.method === 'GET') return send(res, 200, state())
if (req.method !== 'POST') { res.statusCode = 405; return res.end() }
const b = await readBody(req)
const next = { ...live }
if (typeof b.enabled === 'boolean') next.enabled = b.enabled
if ('provider' in b) next.provider = typeof b.provider === 'string' && b.provider ? b.provider : ''
if ('model' in b) next.model = typeof b.model === 'string' && b.model ? b.model : ''
if ('reasoningEffort' in b) next.reasoningEffort = typeof b.reasoningEffort === 'string' && b.reasoningEffort ? b.reasoningEffort : null
if (typeof b.minLevel === 'string' && b.minLevel in LEVEL && b.minLevel !== 'ok') next.minLevel = b.minLevel
live = next
saveSettings(live)
return send(res, 200, state())
} catch (e) { return send(res, 400, { code: 'error', message: String(e?.message ?? e) }) }
},
}), 'omp-advisor: settings route')
})
for (const agent of ctx.agents.list()) install(agent)
ctx.on('agent/created', ({ agent }) => { install(agent) })
ctx.on('agent/disposed', ({ agent }) => { installed.get(agent)?.(); installed.delete(agent) })