wisp-agents 0.13.0 → 0.15.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +25 -1
- package/README.md +17 -4
- package/package.json +2 -2
- package/server/adapters/claude.js +50 -12
- package/server/client.js +1 -1
- package/server/index.js +110 -9
- package/server/machines.js +5 -0
- package/server/mcp-wisp.js +2 -2
- package/server/openapi.js +7 -1
- package/server/protocol.js +4 -0
- package/server/router-priors.json +54 -0
- package/server/router.js +340 -0
- package/server/runner.js +52 -7
- package/server/store.js +10 -2
- package/server/subagents.js +6 -2
- package/web/css/app.css +41 -2
- package/web/js/app.js +173 -25
package/web/js/app.js
CHANGED
|
@@ -62,20 +62,30 @@ const MODES = [
|
|
|
62
62
|
{ id: 'edits', label: 'Edits', icon: 'pen', hint: 'May edit files. Shell commands that need approval are denied.' },
|
|
63
63
|
{ id: 'auto', label: 'Full auto', icon: 'spark', hint: 'No permission prompts at all. Trusted repos only.' },
|
|
64
64
|
];
|
|
65
|
-
const KIND_ICON = { read: 'read', edit: 'pen', run: 'terminal', search: 'search', web: 'web', agent: 'agent', plan: 'plan', tool: 'tool', text: 'text', error: 'warn', ask: 'ask', answer: 'check', wait: 'ask' };
|
|
65
|
+
const KIND_ICON = { read: 'read', edit: 'pen', run: 'terminal', search: 'search', web: 'web', agent: 'agent', plan: 'plan', tool: 'tool', text: 'text', error: 'warn', ask: 'ask', answer: 'check', wait: 'ask', background: 'clock' };
|
|
66
66
|
const STATUS_LABEL = { done: 'Done', partial: 'Partial', blocked: 'Blocked', info: 'Report' };
|
|
67
|
-
const agentName = (id) => S.agents.find((a) => a.id === id)?.name || (id === 'wisp' ? 'wisp' : id);
|
|
67
|
+
const agentName = (id) => S.agents.find((a) => a.id === id)?.name || (id === 'auto' ? 'Auto' : id === 'wisp' ? 'wisp' : id);
|
|
68
|
+
// Auto: not a CLI but wisp's router (server/router.js), which picks harness and model per task.
|
|
69
|
+
// Its "models" are the goal it optimises for; there are no thinking levels to pick.
|
|
70
|
+
const AUTO_GOALS = [
|
|
71
|
+
{ id: 'balanced', label: 'Balanced', hint: 'Best result per dollar' },
|
|
72
|
+
{ id: 'cheap', label: 'Cheap', hint: 'Lowest cost that still gets it done' },
|
|
73
|
+
{ id: 'best', label: 'Best', hint: 'Best result; cost only breaks ties' },
|
|
74
|
+
];
|
|
75
|
+
const AUTO = { id: 'auto', name: 'Auto', available: true, delegates: true, models: AUTO_GOALS, efforts: [], hint: 'wisp picks the harness and model from what worked before' };
|
|
68
76
|
// Harness lists per place: '' is this computer, else a machine (what it reported when it came online).
|
|
69
77
|
const agentsAt = (k) => (k ? (machineById(k)?.state === 'online' && machineById(k).remote?.agents) || null : S.agents);
|
|
70
78
|
// A harness's models and levels: this computer's if it's installed here, else the first machine's that has it.
|
|
71
79
|
function agentById(id) {
|
|
80
|
+
if (id === 'auto') return AUTO;
|
|
72
81
|
const local = S.agents.find((a) => a.id === id);
|
|
73
82
|
if (local?.available) return local;
|
|
74
83
|
return S.machines.map((m) => agentsAt(m.id)?.find((a) => a.id === id && a.available)).find(Boolean) || local;
|
|
75
84
|
}
|
|
76
85
|
// The harnesses you can pick for the places under "Run on": installed on any of them counts (a union).
|
|
77
86
|
// `missing` names the picked places that don't have it. Machines not online yet don't count.
|
|
78
|
-
function harnessesAt(places) {
|
|
87
|
+
function harnessesAt(places, { auto = false } = {}) {
|
|
88
|
+
if (auto) return [AUTO, ...harnessesAt(places)];
|
|
79
89
|
const known = places.map((k) => [k, agentsAt(k)]).filter(([, l]) => l);
|
|
80
90
|
if (!known.length) known.push(['', S.agents]);
|
|
81
91
|
const ids = [...new Set([...S.agents, ...known.flatMap(([, l]) => l)].map((a) => a.id))];
|
|
@@ -99,6 +109,7 @@ const HARNESS_LOGO = {
|
|
|
99
109
|
grok: { file: 'grok', mono: true },
|
|
100
110
|
};
|
|
101
111
|
function harnessLogo(agent) {
|
|
112
|
+
if (agent === 'auto') return `<i class="auto-mark">${icon('spark')}</i>`;
|
|
102
113
|
const l = HARNESS_LOGO[agent];
|
|
103
114
|
const src = l && `/logos/${l.file}.svg`;
|
|
104
115
|
return !l ? icon('spark') : l.mono ? `<i class="mono" style="--logo:url(${src})"></i>` : `<img src="${src}" alt="">`;
|
|
@@ -111,12 +122,19 @@ function effortsFor(agent, model) {
|
|
|
111
122
|
}
|
|
112
123
|
const effortLabel = (e) => ({ xhigh: 'X-High' })[e] || e.charAt(0).toUpperCase() + e.slice(1);
|
|
113
124
|
|
|
114
|
-
function modelBadge(agent, model, effort) {
|
|
125
|
+
function modelBadge(agent, model, effort, route) {
|
|
115
126
|
if (agent === 'wisp') return '';
|
|
116
127
|
const label = (model && agentById(agent)?.models?.find((x) => x.id === model)?.label) || model || 'Default model';
|
|
117
|
-
const tip = `${agentName(agent)} · ${label}${effort ? ` · ${effortLabel(effort)} thinking` : ''}`;
|
|
128
|
+
const tip = route ? routeTip(route) : `${agentName(agent)} · ${label}${effort ? ` · ${effortLabel(effort)} thinking` : ''}`;
|
|
118
129
|
const logo = harnessLogo(agent);
|
|
119
|
-
return `<span class="model-logo" data-tip="${esc(tip)}" aria-label="${esc(tip)}" tabindex="0">${logo}</span>`;
|
|
130
|
+
return `<span class="model-logo${route ? ' routed' : ''}" data-tip="${esc(tip)}" aria-label="${esc(tip)}" tabindex="0">${logo}</span>`;
|
|
131
|
+
}
|
|
132
|
+
// Why Auto picked what it picked, in one line.
|
|
133
|
+
function routeTip(r) {
|
|
134
|
+
const goal = AUTO_GOALS.find((g) => g.id === r.goal)?.label || 'Balanced';
|
|
135
|
+
const record = r.n ? `${Math.round(r.chance * 100)}% on your ${r.type} tasks (${r.n})` : `${Math.round(r.chance * 100)}% expected on ${r.type} tasks`;
|
|
136
|
+
const why = r.trying ? 'new model, trying it out' : r.explored ? 'trying an alternative to learn' : record;
|
|
137
|
+
return `Auto · ${goal} → ${r.label} · ${why}${r.usd ? ` · ~$${r.usd.toFixed(2)}` : ''}`;
|
|
120
138
|
}
|
|
121
139
|
const projectById = (id) => S.config.projects.find((p) => p.id === id);
|
|
122
140
|
|
|
@@ -359,7 +377,7 @@ function renderHead(n, s) {
|
|
|
359
377
|
${pill}
|
|
360
378
|
${s.queued ? `<span class="q-queued" title="Follow-ups queued">+${s.queued}</span>` : ''}
|
|
361
379
|
${s.pr ? prPill(s.pr) : ''}
|
|
362
|
-
${s.team?.length ? teamStack(s.agent, s.team) : modelBadge(s.agent, s.model, s.effort)}
|
|
380
|
+
${s.team?.length ? teamStack(s.agent, s.team) : modelBadge(s.agent, s.model, s.effort, s.route)}
|
|
363
381
|
${s.machine ? machinePill(s.machine) : ''}
|
|
364
382
|
<span class="q-proj">${esc(s.project)}</span>
|
|
365
383
|
<span class="q-time">${ago(s.updatedAt)}</span>
|
|
@@ -642,6 +660,7 @@ function syncWhere() {
|
|
|
642
660
|
// The model is remembered per harness (ids aren't portable between them).
|
|
643
661
|
function savedModel(id) {
|
|
644
662
|
const d = S.config.defaults;
|
|
663
|
+
if (id === 'auto') return AUTO_GOALS.some((g) => g.id === d.models?.auto) ? d.models.auto : 'balanced';
|
|
645
664
|
const want = d.models?.[id] ?? (id === d.agent ? d.model : '');
|
|
646
665
|
const models = agentById(id)?.models || [];
|
|
647
666
|
return models.some((m) => m.id === want) ? want : '';
|
|
@@ -656,8 +675,8 @@ function savedEffort(id, model) {
|
|
|
656
675
|
function renderLauncher() {
|
|
657
676
|
const box = $('#launcher');
|
|
658
677
|
const mode = S.config.defaults.mode || 'edits';
|
|
659
|
-
const here = harnessesAt(launchWhere);
|
|
660
|
-
const chosen = here.find((a) => a.id === (S.config.defaults.agent || '
|
|
678
|
+
const here = harnessesAt(launchWhere, { auto: true });
|
|
679
|
+
const chosen = here.find((a) => a.id === (S.config.defaults.agent || 'auto'));
|
|
661
680
|
let agent = chosen?.available ? chosen : here.find((a) => a.available) || chosen || here[0];
|
|
662
681
|
box.innerHTML = `
|
|
663
682
|
<div class="l-projects"></div>
|
|
@@ -819,8 +838,8 @@ function renderLauncher() {
|
|
|
819
838
|
});
|
|
820
839
|
paintCompare();
|
|
821
840
|
runnerBtn.onclick = () =>
|
|
822
|
-
runnerPicker(runnerBtn, { agent: agent?.id, model: curModel, effort: curEffort, savedModel, savedEffort, places: launchWhere }, (id, model, effort) => {
|
|
823
|
-
agent = harnessesAt(launchWhere).find((a) => a.id === id);
|
|
841
|
+
runnerPicker(runnerBtn, { agent: agent?.id, model: curModel, effort: curEffort, savedModel, savedEffort, places: launchWhere, auto: true }, (id, model, effort) => {
|
|
842
|
+
agent = harnessesAt(launchWhere, { auto: true }).find((a) => a.id === id);
|
|
824
843
|
curModel = model;
|
|
825
844
|
curEffort = effort;
|
|
826
845
|
paintRunner();
|
|
@@ -847,7 +866,7 @@ function renderLauncher() {
|
|
|
847
866
|
if (unset) return openProjects({ focusAdd: true, machine: unset.k || null });
|
|
848
867
|
const off = targets.find((t) => t.m && t.m.state !== 'online');
|
|
849
868
|
if (off) return offlineToast(off.k);
|
|
850
|
-
const wanted = [agent?.id, ...compare.map((c) => c.agent)];
|
|
869
|
+
const wanted = [agent?.id, ...compare.map((c) => c.agent)].filter((id) => id !== 'auto'); // Auto picks from what's there
|
|
851
870
|
for (const t of targets) {
|
|
852
871
|
const list = agentsAt(t.k);
|
|
853
872
|
const missing = list && wanted.find((id) => !list.some((x) => x.id === id && x.available));
|
|
@@ -909,7 +928,7 @@ function renderLauncher() {
|
|
|
909
928
|
// Picking other places (or a machine coming online) changes which harnesses exist: keep the
|
|
910
929
|
// pick if one of them still has it, else move to one that's installed there.
|
|
911
930
|
box.placesChanged = () => {
|
|
912
|
-
const here = harnessesAt(launchWhere);
|
|
931
|
+
const here = harnessesAt(launchWhere, { auto: true }); // Auto works anywhere: it picks from what's there
|
|
913
932
|
if (!agent || here.find((a) => a.id === agent.id)?.available) return paintRunner();
|
|
914
933
|
const next = here.find((a) => a.id === S.config.defaults.agent && a.available) || here.find((a) => a.available);
|
|
915
934
|
if (!next) return paintRunner();
|
|
@@ -1175,7 +1194,7 @@ function teamEditor(anchor, { lead, team, presetId, places }, onChange) {
|
|
|
1175
1194
|
const rb = e.target.closest('.tp-runner');
|
|
1176
1195
|
if (rb) {
|
|
1177
1196
|
const m = members[i];
|
|
1178
|
-
runnerPicker(rb, { agent: m.agent, model: m.model, effort: m.effort, savedModel, savedEffort, places }, (agent, model, effort) => {
|
|
1197
|
+
runnerPicker(rb, { agent: m.agent, model: m.model, effort: m.effort, savedModel, savedEffort, places, auto: true }, (agent, model, effort) => {
|
|
1179
1198
|
Object.assign(m, { agent, model, effort });
|
|
1180
1199
|
paintList();
|
|
1181
1200
|
commit();
|
|
@@ -1823,7 +1842,7 @@ function renderBody(id, { fresh }) {
|
|
|
1823
1842
|
const dockTop = dock?.getBoundingClientRect().top;
|
|
1824
1843
|
const keep = new Set();
|
|
1825
1844
|
let firstNew = null;
|
|
1826
|
-
turns.forEach((t) => {
|
|
1845
|
+
turns.forEach((t, i) => {
|
|
1827
1846
|
const isLatest = t.id === latestId;
|
|
1828
1847
|
const folded = !isLatest && !n.unfolded.has(t.id);
|
|
1829
1848
|
const revKey = `${t.rolledBackAt || 0}:${(t.changes || []).map((c) => (c.reverted ? 1 : 0)).join('')}`;
|
|
@@ -1839,7 +1858,9 @@ function renderBody(id, { fresh }) {
|
|
|
1839
1858
|
n.turnNodes.set(t.id, { key, node, status: t.status });
|
|
1840
1859
|
}
|
|
1841
1860
|
keep.add(t.id);
|
|
1842
|
-
|
|
1861
|
+
// Place the turn without re-inserting a node that's already here: re-parenting restarts its
|
|
1862
|
+
// CSS animations, so a refetch would replay the whole card's entrance.
|
|
1863
|
+
if (turnsEl.children[i] !== node) turnsEl.insertBefore(node, turnsEl.children[i] || null);
|
|
1843
1864
|
});
|
|
1844
1865
|
for (const [tid, v] of n.turnNodes) if (!keep.has(tid)) (v.node.remove(), n.turnNodes.delete(tid));
|
|
1845
1866
|
|
|
@@ -2065,15 +2086,18 @@ function renderTrail(s, t) {
|
|
|
2065
2086
|
const node = el(`
|
|
2066
2087
|
<div class="trail">
|
|
2067
2088
|
<div class="trail-bar">
|
|
2068
|
-
${parts.map(([ic, l, n]) => `<span class="tb" title="${n} ${l}">${icon(ic)}${n}</span>`).join('')}
|
|
2089
|
+
${acts.length ? `<button class="tb-acts" data-tab="activity" title="Show activity">${parts.length ? parts.map(([ic, l, n]) => `<span class="tb" title="${n} ${l}">${icon(ic)}${n}</span>`).join('') : 'Activity'}</button>` : ''}
|
|
2069
2090
|
${t.endedAt ? `<span class="tb">${fmtDuration(t.endedAt - t.startedAt)}</span>` : ''}
|
|
2070
2091
|
${st.costUsd ? `<span class="tb">$${st.costUsd.toFixed(2)}</span>` : ''}
|
|
2071
2092
|
<span class="grow"></span>
|
|
2072
|
-
${canRollback ? `<button class="link xs cp-rollback ${rolledBack ? 'rolled-back' : ''}" data-a="rollback" title="${rolledBack ? 'Re-apply changes from this turn' : `Restore working tree to before this turn${t.checkpoint?.head ? ` (${t.checkpoint.head.slice(0, 7)})` : ''}`}">${icon(rolledBack ? 'rerun' : 'replay')}${rolledBack ? 'Re-apply turn' : 'Restore to before this turn'}</button>` : ''}
|
|
2073
2093
|
${t.changes?.length ? `<button class="link xs" data-tab="changes">${t.changes.length} file${t.changes.length > 1 ? 's' : ''} <span class="plus">+${totals.add}</span> <span class="minus">−${totals.del}</span></button>` : ''}
|
|
2074
|
-
|
|
2075
|
-
|
|
2076
|
-
|
|
2094
|
+
<span class="tb-tools">
|
|
2095
|
+
${t.presentation?.notes ? `<button class="tb-btn" data-tab="notes" title="Agent notes" aria-label="Agent notes">${icon('text')}</button>` : ''}
|
|
2096
|
+
${canRollback ? (rolledBack
|
|
2097
|
+
? `<button class="link xs cp-rollback rolled-back" data-a="rollback" title="Re-apply changes from this turn">${icon('rerun')}Re-apply</button>`
|
|
2098
|
+
: `<button class="tb-btn cp-rollback" data-a="rollback" title="Restore working tree to before this turn${t.checkpoint?.head ? ` (${t.checkpoint.head.slice(0, 7)})` : ''}" aria-label="Restore to before this turn">${icon('replay')}</button>`) : ''}
|
|
2099
|
+
${feedbackOn() ? `<span class="rate ${rated ? 'rated' : ''}">${['up', 'down'].map((r) => `<button class="rate-btn ${rated === r ? 'on' : ''}" data-rate="${r}" title="${r === 'up' ? 'Good result' : 'Bad result'}: tell the wisp team" aria-pressed="${rated === r}">${icon(`thumb-${r}`)}</button>`).join('')}</span>` : ''}
|
|
2100
|
+
</span>
|
|
2077
2101
|
</div>
|
|
2078
2102
|
<div class="trail-body" hidden></div>
|
|
2079
2103
|
</div>`);
|
|
@@ -2500,11 +2524,12 @@ function rename(id) {
|
|
|
2500
2524
|
// right, and its thinking levels underneath. Clicking (or arrowing to) a harness previews its
|
|
2501
2525
|
// models; picking a model commits harness + model, picking a thinking level commits it in place.
|
|
2502
2526
|
// places: the "Run on" picks, so the list is what's installed there rather than only on this computer.
|
|
2503
|
-
function runnerPicker(anchor, { agent, model, effort, savedModel, savedEffort, places }, pick) {
|
|
2527
|
+
function runnerPicker(anchor, { agent, model, effort, savedModel, savedEffort, places, auto = false }, pick) {
|
|
2504
2528
|
const open = $('.picker');
|
|
2505
2529
|
if (open) return open.close();
|
|
2506
|
-
const agents = places ? harnessesAt(places) : S.agents;
|
|
2530
|
+
const agents = places ? harnessesAt(places, { auto }) : auto ? [AUTO, ...S.agents] : S.agents;
|
|
2507
2531
|
const sub = (a) => {
|
|
2532
|
+
if (a.id === 'auto') return 'Picks harness and model for you';
|
|
2508
2533
|
if (!a.available) return places?.length > 1 || places?.[0] ? 'Not installed there' : 'Not installed';
|
|
2509
2534
|
const models = (a.models || []).length > 1 ? `${a.models.length - 1} models` : 'Default model';
|
|
2510
2535
|
return a.missing?.length ? `Not on ${a.missing.map(placeName).join(', ')}` : models;
|
|
@@ -2575,6 +2600,17 @@ function runnerPicker(anchor, { agent, model, effort, savedModel, savedEffort, p
|
|
|
2575
2600
|
const models = a?.models || [{ id: '', label: 'Default' }];
|
|
2576
2601
|
const current = focusAgent === agent ? model : savedModel(focusAgent);
|
|
2577
2602
|
input.placeholder = `Search ${a?.name || ''} models`;
|
|
2603
|
+
if (a?.id === 'auto') {
|
|
2604
|
+
list.innerHTML = `<div class="pk-group"><div class="pk-ghead">Goal</div>${AUTO_GOALS.map((g) => `<button class="pk-m${g.id === current ? ' on' : ''}" role="option" aria-selected="${g.id === current}" data-id="${g.id}"><span class="pk-text"><b>${g.label}</b><small>${g.hint}</small></span>${icon('check', 'pk-check')}</button>`).join('')}</div><p class="pk-note">wisp picks the harness and model for each task from what worked before, and learns from how it went. ${'<a href="#" class="pk-auto-more">How Auto picks</a>'}</p><div class="pk-empty" hidden>No models match</div>`;
|
|
2605
|
+
$('.pk-search', node).hidden = true;
|
|
2606
|
+
$('.pk-auto-more', list).onclick = (e) => {
|
|
2607
|
+
e.preventDefault();
|
|
2608
|
+
close();
|
|
2609
|
+
openSettings({ pane: 'auto' });
|
|
2610
|
+
};
|
|
2611
|
+
paintEffort();
|
|
2612
|
+
return;
|
|
2613
|
+
}
|
|
2578
2614
|
$('.pk-search', node).hidden = models.length <= 6;
|
|
2579
2615
|
// opencode-style ids (provider/model) group by provider.
|
|
2580
2616
|
const groups = new Map();
|
|
@@ -2856,7 +2892,7 @@ async function rerun(id) {
|
|
|
2856
2892
|
modeBtn.onclick = () => modeMenu(modeBtn, mode, (m) => ((mode = m), paintMode(), ta.focus()));
|
|
2857
2893
|
// Picks here only apply to this rerun; the launcher's remembered defaults are left alone.
|
|
2858
2894
|
runnerBtn.onclick = () =>
|
|
2859
|
-
runnerPicker(runnerBtn, { agent, model, effort, savedModel, savedEffort }, (a, m, e) => {
|
|
2895
|
+
runnerPicker(runnerBtn, { agent, model, effort, savedModel, savedEffort, auto: true }, (a, m, e) => {
|
|
2860
2896
|
[agent, model, effort] = [a, m, e];
|
|
2861
2897
|
paintRunner();
|
|
2862
2898
|
});
|
|
@@ -3447,7 +3483,7 @@ function openAutomations({ tab = 'recipes', edit = null, host } = {}) {
|
|
|
3447
3483
|
const [btn, reset] = wrap.children;
|
|
3448
3484
|
const paint = () => (paintRunnerBtn(btn, draft.agent, draft.model, draft.effort), (reset.hidden = !draft.agent));
|
|
3449
3485
|
btn.onclick = () =>
|
|
3450
|
-
runnerPicker(btn, { agent: draft.agent || S.config.defaults.agent, model: draft.model, effort: draft.effort, savedModel, savedEffort }, (a, m, e) => {
|
|
3486
|
+
runnerPicker(btn, { agent: draft.agent || S.config.defaults.agent, model: draft.model, effort: draft.effort, savedModel, savedEffort, auto: true }, (a, m, e) => {
|
|
3451
3487
|
Object.assign(draft, { agent: a, model: m, effort: e });
|
|
3452
3488
|
paint();
|
|
3453
3489
|
});
|
|
@@ -4149,6 +4185,7 @@ const SETTINGS_PANES = [
|
|
|
4149
4185
|
{ id: 'general', label: 'General', icon: 'settings', lede: 'Where files open, where you take over, and how tasks start.' },
|
|
4150
4186
|
{ id: 'notifications', label: 'Notifications', icon: 'bolt', lede: 'Hear about questions and finished tasks, here or on your phone.' },
|
|
4151
4187
|
{ id: 'agents', label: 'Agents', icon: 'agent', lede: 'The harnesses wisp found on this machine. Pick one next to the model in the launcher.' },
|
|
4188
|
+
{ id: 'auto', label: 'Auto', icon: 'spark', lede: 'wisp picks the harness and model for each task, and learns from how it went.' },
|
|
4152
4189
|
{ id: 'machines', label: 'Machines', icon: 'server', lede: 'Other computers that run agents for you, over SSH.' },
|
|
4153
4190
|
{ id: 'memory', label: 'Memory', icon: 'spark', lede: "Learned from your sessions and added to every agent's prompt. What you ask always wins." },
|
|
4154
4191
|
{ id: 'automations', label: 'Automations', icon: 'play', lede: 'Saved prompts, and the triggers that launch them for you.' },
|
|
@@ -4214,6 +4251,16 @@ function openSettings(opts) {
|
|
|
4214
4251
|
${row('Installed something new?', '<span class="recheck-hint"></span>', `<button class="ghost sm" id="recheck">${icon('replay')}Re-check</button>`)}
|
|
4215
4252
|
</div>
|
|
4216
4253
|
</section>
|
|
4254
|
+
<section class="sx-pane" data-pane="auto">
|
|
4255
|
+
<div class="sx-group">
|
|
4256
|
+
${row('Goal', 'What Auto optimises for. Also in the launcher, next to Auto', `<span class="pk-seg ro-goal">${AUTO_GOALS.map((g) => `<button data-goal="${g.id}" title="${esc(g.hint)}">${g.label}</button>`).join('')}</span>`)}
|
|
4257
|
+
${row('Try new models', 'A new model or harness gets a few tasks that aren’t marked important before Auto relies on it', '<button class="switch" id="roExplore" role="switch"></button>')}
|
|
4258
|
+
${row('Harnesses it may use', 'Leave all off to let Auto use every one you have', '<span class="ro-allowed"></span>')}
|
|
4259
|
+
</div>
|
|
4260
|
+
<div class="sx-group">
|
|
4261
|
+
${row('What it learned', '<span class="ro-hint"></span>', '<button class="ghost sm" id="openRouter">Open</button>')}
|
|
4262
|
+
</div>
|
|
4263
|
+
</section>
|
|
4217
4264
|
<section class="sx-pane" data-pane="machines">
|
|
4218
4265
|
<div class="sx-group">
|
|
4219
4266
|
${row('Machines', '<span class="machines-hint"></span>', '<button class="ghost sm" id="openMachines">Manage</button>')}
|
|
@@ -4392,6 +4439,52 @@ function openSettings(opts) {
|
|
|
4392
4439
|
renderLauncher();
|
|
4393
4440
|
};
|
|
4394
4441
|
paintWt();
|
|
4442
|
+
// Auto: goal, exploring and allowed harnesses here; the full record in its own view.
|
|
4443
|
+
const paintAuto = async () => {
|
|
4444
|
+
const r = await api('/router').catch(() => null);
|
|
4445
|
+
if (!r || !node.isConnected) return;
|
|
4446
|
+
$$('.ro-goal button', node).forEach((b) => b.classList.toggle('on', b.dataset.goal === r.goal));
|
|
4447
|
+
$('#roExplore', node).setAttribute('aria-checked', r.settings.explore);
|
|
4448
|
+
const allowed = r.settings.allowed || [];
|
|
4449
|
+
$('.ro-allowed', node).innerHTML = S.agents
|
|
4450
|
+
.filter((a) => a.available)
|
|
4451
|
+
.map((a) => `<button class="chip-tog${allowed.includes(a.id) ? ' on' : ''}" data-id="${esc(a.id)}" aria-pressed="${allowed.includes(a.id)}" title="${esc(a.name)}"><span class="model-logo">${harnessLogo(a.id)}</span>${esc(a.name)}</button>`)
|
|
4452
|
+
.join('');
|
|
4453
|
+
$('.ro-hint', node).textContent = r.learned
|
|
4454
|
+
? `${r.learned} task${r.learned === 1 ? '' : 's'} learned from · ${r.arms.filter((a) => a.state !== 'gone').length} harness · model pairs`
|
|
4455
|
+
: `Nothing of yours yet · starting from wisp-bench (${r.priors.version})`;
|
|
4456
|
+
};
|
|
4457
|
+
const patchAuto = async (body) => {
|
|
4458
|
+
try {
|
|
4459
|
+
await api('/router', { method: 'PATCH', body });
|
|
4460
|
+
await loadConfig();
|
|
4461
|
+
renderLauncher();
|
|
4462
|
+
} catch (err) {
|
|
4463
|
+
toast(esc(err.message), { tone: 'bad' });
|
|
4464
|
+
}
|
|
4465
|
+
paintAuto();
|
|
4466
|
+
};
|
|
4467
|
+
$('.ro-goal', node).onclick = (e) => {
|
|
4468
|
+
const b = e.target.closest('[data-goal]');
|
|
4469
|
+
if (b) {
|
|
4470
|
+
$$('.ro-goal button', node).forEach((x) => x.classList.toggle('on', x === b));
|
|
4471
|
+
patchAuto({ goal: b.dataset.goal });
|
|
4472
|
+
}
|
|
4473
|
+
};
|
|
4474
|
+
$('#roExplore', node).onclick = (e) => {
|
|
4475
|
+
const on = e.currentTarget.getAttribute('aria-checked') !== 'true';
|
|
4476
|
+
e.currentTarget.setAttribute('aria-checked', on);
|
|
4477
|
+
patchAuto({ explore: on });
|
|
4478
|
+
};
|
|
4479
|
+
$('.ro-allowed', node).onclick = (e) => {
|
|
4480
|
+
const b = e.target.closest('[data-id]');
|
|
4481
|
+
if (!b) return;
|
|
4482
|
+
b.classList.toggle('on');
|
|
4483
|
+
b.setAttribute('aria-pressed', b.classList.contains('on'));
|
|
4484
|
+
patchAuto({ allowed: $$('.ro-allowed .on', node).map((x) => x.dataset.id) });
|
|
4485
|
+
};
|
|
4486
|
+
node.paintAuto = paintAuto;
|
|
4487
|
+
|
|
4395
4488
|
// The one-line summaries; painted again on the way back from a view that may have changed them.
|
|
4396
4489
|
const paintHints = () => {
|
|
4397
4490
|
const r = S.config.recipes?.length || 0;
|
|
@@ -4412,8 +4505,10 @@ function openSettings(opts) {
|
|
|
4412
4505
|
$('.machines-hint', node).textContent = machinesSummary();
|
|
4413
4506
|
api('/remote').then((x) => ($('.phone-hint', node).textContent = phoneSummary(x) === 'Off' ? 'ntfy or Telegram: answer questions from your phone' : phoneSummary(x))).catch(() => {});
|
|
4414
4507
|
paintLearn();
|
|
4508
|
+
paintAuto();
|
|
4415
4509
|
};
|
|
4416
4510
|
paintHints();
|
|
4511
|
+
$('#openRouter', node).onclick = () => openRouter({ host: host('auto') });
|
|
4417
4512
|
$('#openMemory', node).onclick = () => openMemory({ host: host('memory') });
|
|
4418
4513
|
$$('[data-auto]', node).forEach((b) => (b.onclick = () => openAutomations({ tab: b.dataset.auto, host: host('automations') })));
|
|
4419
4514
|
$('#openUsage', node).onclick = () => openUsage({ days: 7, host: host('usage') });
|
|
@@ -4421,6 +4516,52 @@ function openSettings(opts) {
|
|
|
4421
4516
|
$('#openMachines', node).onclick = () => openMachines({ host: host('machines') });
|
|
4422
4517
|
}
|
|
4423
4518
|
|
|
4519
|
+
// --- Auto: what the router learned ---------------------------------------------------------
|
|
4520
|
+
|
|
4521
|
+
const ARM_STATE = { ready: ['Ready', 'ok'], new: ['Trying out', 'warn'], untried: ['No data yet', ''], gone: ['Not installed', 'bad'] };
|
|
4522
|
+
async function openRouter({ host } = {}) {
|
|
4523
|
+
const node = el(`<div class="modal router-modal">
|
|
4524
|
+
<header><h2>Auto</h2><button class="icon-btn close">${icon('x')}</button></header>
|
|
4525
|
+
<div class="ro-body"><div class="it-loading"><span class="orb tiny"></span>Loading…</div></div>
|
|
4526
|
+
</div>`);
|
|
4527
|
+
const close = present(node, host);
|
|
4528
|
+
const pctOf = (x) => `${Math.round(x * 100)}%`;
|
|
4529
|
+
const outcome = (r) => (r === null ? '<span class="dim">waiting</span>' : r >= 0.9 ? '<span class="ok">worked</span>' : r >= 0.5 ? '<span>finished</span>' : r >= 0.25 ? '<span class="warn">partly</span>' : '<span class="bad">didn’t work</span>');
|
|
4530
|
+
const render = async () => {
|
|
4531
|
+
const r = await api('/router').catch((err) => (toast(esc(err.message), { tone: 'bad' }), null));
|
|
4532
|
+
if (!r || !node.isConnected) return;
|
|
4533
|
+
const arms = r.arms.filter((a) => a.state !== 'gone' || a.n);
|
|
4534
|
+
$('.ro-body', node).innerHTML = `
|
|
4535
|
+
<p class="hint">For each task Auto estimates every harness · model’s chance of doing it well, from wisp-bench (${esc(r.priors.version)}) and then more and more from your own tasks: merged PRs, 👍 / 👎, rollbacks, follow-ups and how the run ended. It picks the best value for your goal (<b>${esc(AUTO_GOALS.find((g) => g.id === r.goal)?.label || '')}</b>), now and then trying an alternative so the estimates stay honest.</p>
|
|
4536
|
+
<h4>Harness · model</h4>
|
|
4537
|
+
<table class="us-table ro-table"><thead><tr><th></th><th>Works</th><th>Your tasks</th><th>~Cost</th><th>~Time</th><th></th></tr></thead><tbody>${arms
|
|
4538
|
+
.map((a) => {
|
|
4539
|
+
const [state, tone] = ARM_STATE[a.state] || ARM_STATE.ready;
|
|
4540
|
+
const types = Object.entries(a.byType).map(([t, x]) => `${t} ${pctOf(x.chance)} (${x.n})`).join(' · ');
|
|
4541
|
+
return `<tr><td><span class="us-runner"><span class="model-logo">${harnessLogo(a.agent)}</span>${esc(a.label)}</span>${types ? `<small class="ro-types">${esc(types)}</small>` : ''}</td>${a.state === 'untried' ? '<td>—</td><td>—</td><td>—</td><td>—</td>' : `<td><b>${pctOf(a.chance)}</b></td><td>${a.n || '—'}</td><td>${a.usd ? `$${a.usd.toFixed(2)}` : '$0'}</td><td>${a.minutes}m</td>`}<td><span class="sx-state ${tone}"><i class="dot ${tone}"></i>${state}</span></td></tr>`;
|
|
4542
|
+
})
|
|
4543
|
+
.join('')}</tbody></table>
|
|
4544
|
+
<h4>Recent picks</h4>
|
|
4545
|
+
${r.recent.length
|
|
4546
|
+
? `<ul class="ro-recent">${r.recent
|
|
4547
|
+
.map((x) => {
|
|
4548
|
+
const s = S.sessions.get(x.id);
|
|
4549
|
+
return `<li><span class="model-logo">${harnessLogo(x.key.split(':')[0])}</span><span class="ro-title">${esc(s?.title || 'Task')}</span><small>${esc(x.label)} · ${esc(x.type)}${x.trying ? ' · trying' : x.explored ? ' · exploring' : ''}</small>${outcome(x.r)}</li>`;
|
|
4550
|
+
})
|
|
4551
|
+
.join('')}</ul>`
|
|
4552
|
+
: '<p class="mem-empty">Nothing yet. Launch with Auto and its picks show up here.</p>'}
|
|
4553
|
+
<div class="ro-foot"><button class="ghost sm danger" id="roReset">Forget what Auto learned</button><span class="hint">The wisp-bench starting point stays.</span></div>`;
|
|
4554
|
+
$('#roReset', node).onclick = async () => {
|
|
4555
|
+
if (!confirm('Forget what Auto learned from your tasks?')) return;
|
|
4556
|
+
await api('/router/reset', { method: 'POST', body: {} }).catch((err) => toast(esc(err.message), { tone: 'bad' }));
|
|
4557
|
+
render();
|
|
4558
|
+
};
|
|
4559
|
+
};
|
|
4560
|
+
node.refresh = render;
|
|
4561
|
+
render();
|
|
4562
|
+
return close;
|
|
4563
|
+
}
|
|
4564
|
+
|
|
4424
4565
|
// --- memory: what the background retros learned ---------------------------------------------
|
|
4425
4566
|
|
|
4426
4567
|
const MEM_KINDS = [
|
|
@@ -4904,6 +5045,13 @@ function connect() {
|
|
|
4904
5045
|
es.addEventListener('remote', () => $('.phone-modal')?.refresh());
|
|
4905
5046
|
es.addEventListener('machines', (e) => setMachines(JSON.parse(e.data)));
|
|
4906
5047
|
es.addEventListener('update', (e) => renderUpdate(JSON.parse(e.data)));
|
|
5048
|
+
// A new harness or model Auto can use: it gets a few tasks before Auto relies on it.
|
|
5049
|
+
es.addEventListener('router', (e) => {
|
|
5050
|
+
const { added = [] } = JSON.parse(e.data || '{}');
|
|
5051
|
+
$('.router-modal')?.refresh();
|
|
5052
|
+
$('.settings-modal')?.paintAuto?.();
|
|
5053
|
+
if (added.length) toast(`New for Auto: <b>${added.map((a) => esc(a.label)).join('</b>, <b>')}</b>${added.some((a) => a.state === 'new') ? ' · it will try it on a few tasks' : ''}`, { action: { label: 'Auto settings', run: () => openSettings({ pane: 'auto' }) }, timeout: 8000 });
|
|
5054
|
+
});
|
|
4907
5055
|
es.addEventListener('preferences', () => {
|
|
4908
5056
|
$('.memory-modal')?.refresh();
|
|
4909
5057
|
$('.settings-modal')?.paintLearn?.();
|