wisp-agents 0.13.0 → 0.14.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 +14 -1
- package/README.md +16 -3
- 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 +30 -0
- package/web/js/app.js +161 -18
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();
|
|
@@ -2500,11 +2519,12 @@ function rename(id) {
|
|
|
2500
2519
|
// right, and its thinking levels underneath. Clicking (or arrowing to) a harness previews its
|
|
2501
2520
|
// models; picking a model commits harness + model, picking a thinking level commits it in place.
|
|
2502
2521
|
// 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) {
|
|
2522
|
+
function runnerPicker(anchor, { agent, model, effort, savedModel, savedEffort, places, auto = false }, pick) {
|
|
2504
2523
|
const open = $('.picker');
|
|
2505
2524
|
if (open) return open.close();
|
|
2506
|
-
const agents = places ? harnessesAt(places) : S.agents;
|
|
2525
|
+
const agents = places ? harnessesAt(places, { auto }) : auto ? [AUTO, ...S.agents] : S.agents;
|
|
2507
2526
|
const sub = (a) => {
|
|
2527
|
+
if (a.id === 'auto') return 'Picks harness and model for you';
|
|
2508
2528
|
if (!a.available) return places?.length > 1 || places?.[0] ? 'Not installed there' : 'Not installed';
|
|
2509
2529
|
const models = (a.models || []).length > 1 ? `${a.models.length - 1} models` : 'Default model';
|
|
2510
2530
|
return a.missing?.length ? `Not on ${a.missing.map(placeName).join(', ')}` : models;
|
|
@@ -2575,6 +2595,17 @@ function runnerPicker(anchor, { agent, model, effort, savedModel, savedEffort, p
|
|
|
2575
2595
|
const models = a?.models || [{ id: '', label: 'Default' }];
|
|
2576
2596
|
const current = focusAgent === agent ? model : savedModel(focusAgent);
|
|
2577
2597
|
input.placeholder = `Search ${a?.name || ''} models`;
|
|
2598
|
+
if (a?.id === 'auto') {
|
|
2599
|
+
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>`;
|
|
2600
|
+
$('.pk-search', node).hidden = true;
|
|
2601
|
+
$('.pk-auto-more', list).onclick = (e) => {
|
|
2602
|
+
e.preventDefault();
|
|
2603
|
+
close();
|
|
2604
|
+
openSettings({ pane: 'auto' });
|
|
2605
|
+
};
|
|
2606
|
+
paintEffort();
|
|
2607
|
+
return;
|
|
2608
|
+
}
|
|
2578
2609
|
$('.pk-search', node).hidden = models.length <= 6;
|
|
2579
2610
|
// opencode-style ids (provider/model) group by provider.
|
|
2580
2611
|
const groups = new Map();
|
|
@@ -2856,7 +2887,7 @@ async function rerun(id) {
|
|
|
2856
2887
|
modeBtn.onclick = () => modeMenu(modeBtn, mode, (m) => ((mode = m), paintMode(), ta.focus()));
|
|
2857
2888
|
// Picks here only apply to this rerun; the launcher's remembered defaults are left alone.
|
|
2858
2889
|
runnerBtn.onclick = () =>
|
|
2859
|
-
runnerPicker(runnerBtn, { agent, model, effort, savedModel, savedEffort }, (a, m, e) => {
|
|
2890
|
+
runnerPicker(runnerBtn, { agent, model, effort, savedModel, savedEffort, auto: true }, (a, m, e) => {
|
|
2860
2891
|
[agent, model, effort] = [a, m, e];
|
|
2861
2892
|
paintRunner();
|
|
2862
2893
|
});
|
|
@@ -3447,7 +3478,7 @@ function openAutomations({ tab = 'recipes', edit = null, host } = {}) {
|
|
|
3447
3478
|
const [btn, reset] = wrap.children;
|
|
3448
3479
|
const paint = () => (paintRunnerBtn(btn, draft.agent, draft.model, draft.effort), (reset.hidden = !draft.agent));
|
|
3449
3480
|
btn.onclick = () =>
|
|
3450
|
-
runnerPicker(btn, { agent: draft.agent || S.config.defaults.agent, model: draft.model, effort: draft.effort, savedModel, savedEffort }, (a, m, e) => {
|
|
3481
|
+
runnerPicker(btn, { agent: draft.agent || S.config.defaults.agent, model: draft.model, effort: draft.effort, savedModel, savedEffort, auto: true }, (a, m, e) => {
|
|
3451
3482
|
Object.assign(draft, { agent: a, model: m, effort: e });
|
|
3452
3483
|
paint();
|
|
3453
3484
|
});
|
|
@@ -4149,6 +4180,7 @@ const SETTINGS_PANES = [
|
|
|
4149
4180
|
{ id: 'general', label: 'General', icon: 'settings', lede: 'Where files open, where you take over, and how tasks start.' },
|
|
4150
4181
|
{ id: 'notifications', label: 'Notifications', icon: 'bolt', lede: 'Hear about questions and finished tasks, here or on your phone.' },
|
|
4151
4182
|
{ id: 'agents', label: 'Agents', icon: 'agent', lede: 'The harnesses wisp found on this machine. Pick one next to the model in the launcher.' },
|
|
4183
|
+
{ id: 'auto', label: 'Auto', icon: 'spark', lede: 'wisp picks the harness and model for each task, and learns from how it went.' },
|
|
4152
4184
|
{ id: 'machines', label: 'Machines', icon: 'server', lede: 'Other computers that run agents for you, over SSH.' },
|
|
4153
4185
|
{ id: 'memory', label: 'Memory', icon: 'spark', lede: "Learned from your sessions and added to every agent's prompt. What you ask always wins." },
|
|
4154
4186
|
{ id: 'automations', label: 'Automations', icon: 'play', lede: 'Saved prompts, and the triggers that launch them for you.' },
|
|
@@ -4214,6 +4246,16 @@ function openSettings(opts) {
|
|
|
4214
4246
|
${row('Installed something new?', '<span class="recheck-hint"></span>', `<button class="ghost sm" id="recheck">${icon('replay')}Re-check</button>`)}
|
|
4215
4247
|
</div>
|
|
4216
4248
|
</section>
|
|
4249
|
+
<section class="sx-pane" data-pane="auto">
|
|
4250
|
+
<div class="sx-group">
|
|
4251
|
+
${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>`)}
|
|
4252
|
+
${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>')}
|
|
4253
|
+
${row('Harnesses it may use', 'Leave all off to let Auto use every one you have', '<span class="ro-allowed"></span>')}
|
|
4254
|
+
</div>
|
|
4255
|
+
<div class="sx-group">
|
|
4256
|
+
${row('What it learned', '<span class="ro-hint"></span>', '<button class="ghost sm" id="openRouter">Open</button>')}
|
|
4257
|
+
</div>
|
|
4258
|
+
</section>
|
|
4217
4259
|
<section class="sx-pane" data-pane="machines">
|
|
4218
4260
|
<div class="sx-group">
|
|
4219
4261
|
${row('Machines', '<span class="machines-hint"></span>', '<button class="ghost sm" id="openMachines">Manage</button>')}
|
|
@@ -4392,6 +4434,52 @@ function openSettings(opts) {
|
|
|
4392
4434
|
renderLauncher();
|
|
4393
4435
|
};
|
|
4394
4436
|
paintWt();
|
|
4437
|
+
// Auto: goal, exploring and allowed harnesses here; the full record in its own view.
|
|
4438
|
+
const paintAuto = async () => {
|
|
4439
|
+
const r = await api('/router').catch(() => null);
|
|
4440
|
+
if (!r || !node.isConnected) return;
|
|
4441
|
+
$$('.ro-goal button', node).forEach((b) => b.classList.toggle('on', b.dataset.goal === r.goal));
|
|
4442
|
+
$('#roExplore', node).setAttribute('aria-checked', r.settings.explore);
|
|
4443
|
+
const allowed = r.settings.allowed || [];
|
|
4444
|
+
$('.ro-allowed', node).innerHTML = S.agents
|
|
4445
|
+
.filter((a) => a.available)
|
|
4446
|
+
.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>`)
|
|
4447
|
+
.join('');
|
|
4448
|
+
$('.ro-hint', node).textContent = r.learned
|
|
4449
|
+
? `${r.learned} task${r.learned === 1 ? '' : 's'} learned from · ${r.arms.filter((a) => a.state !== 'gone').length} harness · model pairs`
|
|
4450
|
+
: `Nothing of yours yet · starting from wisp-bench (${r.priors.version})`;
|
|
4451
|
+
};
|
|
4452
|
+
const patchAuto = async (body) => {
|
|
4453
|
+
try {
|
|
4454
|
+
await api('/router', { method: 'PATCH', body });
|
|
4455
|
+
await loadConfig();
|
|
4456
|
+
renderLauncher();
|
|
4457
|
+
} catch (err) {
|
|
4458
|
+
toast(esc(err.message), { tone: 'bad' });
|
|
4459
|
+
}
|
|
4460
|
+
paintAuto();
|
|
4461
|
+
};
|
|
4462
|
+
$('.ro-goal', node).onclick = (e) => {
|
|
4463
|
+
const b = e.target.closest('[data-goal]');
|
|
4464
|
+
if (b) {
|
|
4465
|
+
$$('.ro-goal button', node).forEach((x) => x.classList.toggle('on', x === b));
|
|
4466
|
+
patchAuto({ goal: b.dataset.goal });
|
|
4467
|
+
}
|
|
4468
|
+
};
|
|
4469
|
+
$('#roExplore', node).onclick = (e) => {
|
|
4470
|
+
const on = e.currentTarget.getAttribute('aria-checked') !== 'true';
|
|
4471
|
+
e.currentTarget.setAttribute('aria-checked', on);
|
|
4472
|
+
patchAuto({ explore: on });
|
|
4473
|
+
};
|
|
4474
|
+
$('.ro-allowed', node).onclick = (e) => {
|
|
4475
|
+
const b = e.target.closest('[data-id]');
|
|
4476
|
+
if (!b) return;
|
|
4477
|
+
b.classList.toggle('on');
|
|
4478
|
+
b.setAttribute('aria-pressed', b.classList.contains('on'));
|
|
4479
|
+
patchAuto({ allowed: $$('.ro-allowed .on', node).map((x) => x.dataset.id) });
|
|
4480
|
+
};
|
|
4481
|
+
node.paintAuto = paintAuto;
|
|
4482
|
+
|
|
4395
4483
|
// The one-line summaries; painted again on the way back from a view that may have changed them.
|
|
4396
4484
|
const paintHints = () => {
|
|
4397
4485
|
const r = S.config.recipes?.length || 0;
|
|
@@ -4412,8 +4500,10 @@ function openSettings(opts) {
|
|
|
4412
4500
|
$('.machines-hint', node).textContent = machinesSummary();
|
|
4413
4501
|
api('/remote').then((x) => ($('.phone-hint', node).textContent = phoneSummary(x) === 'Off' ? 'ntfy or Telegram: answer questions from your phone' : phoneSummary(x))).catch(() => {});
|
|
4414
4502
|
paintLearn();
|
|
4503
|
+
paintAuto();
|
|
4415
4504
|
};
|
|
4416
4505
|
paintHints();
|
|
4506
|
+
$('#openRouter', node).onclick = () => openRouter({ host: host('auto') });
|
|
4417
4507
|
$('#openMemory', node).onclick = () => openMemory({ host: host('memory') });
|
|
4418
4508
|
$$('[data-auto]', node).forEach((b) => (b.onclick = () => openAutomations({ tab: b.dataset.auto, host: host('automations') })));
|
|
4419
4509
|
$('#openUsage', node).onclick = () => openUsage({ days: 7, host: host('usage') });
|
|
@@ -4421,6 +4511,52 @@ function openSettings(opts) {
|
|
|
4421
4511
|
$('#openMachines', node).onclick = () => openMachines({ host: host('machines') });
|
|
4422
4512
|
}
|
|
4423
4513
|
|
|
4514
|
+
// --- Auto: what the router learned ---------------------------------------------------------
|
|
4515
|
+
|
|
4516
|
+
const ARM_STATE = { ready: ['Ready', 'ok'], new: ['Trying out', 'warn'], untried: ['No data yet', ''], gone: ['Not installed', 'bad'] };
|
|
4517
|
+
async function openRouter({ host } = {}) {
|
|
4518
|
+
const node = el(`<div class="modal router-modal">
|
|
4519
|
+
<header><h2>Auto</h2><button class="icon-btn close">${icon('x')}</button></header>
|
|
4520
|
+
<div class="ro-body"><div class="it-loading"><span class="orb tiny"></span>Loading…</div></div>
|
|
4521
|
+
</div>`);
|
|
4522
|
+
const close = present(node, host);
|
|
4523
|
+
const pctOf = (x) => `${Math.round(x * 100)}%`;
|
|
4524
|
+
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>');
|
|
4525
|
+
const render = async () => {
|
|
4526
|
+
const r = await api('/router').catch((err) => (toast(esc(err.message), { tone: 'bad' }), null));
|
|
4527
|
+
if (!r || !node.isConnected) return;
|
|
4528
|
+
const arms = r.arms.filter((a) => a.state !== 'gone' || a.n);
|
|
4529
|
+
$('.ro-body', node).innerHTML = `
|
|
4530
|
+
<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>
|
|
4531
|
+
<h4>Harness · model</h4>
|
|
4532
|
+
<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
|
|
4533
|
+
.map((a) => {
|
|
4534
|
+
const [state, tone] = ARM_STATE[a.state] || ARM_STATE.ready;
|
|
4535
|
+
const types = Object.entries(a.byType).map(([t, x]) => `${t} ${pctOf(x.chance)} (${x.n})`).join(' · ');
|
|
4536
|
+
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>`;
|
|
4537
|
+
})
|
|
4538
|
+
.join('')}</tbody></table>
|
|
4539
|
+
<h4>Recent picks</h4>
|
|
4540
|
+
${r.recent.length
|
|
4541
|
+
? `<ul class="ro-recent">${r.recent
|
|
4542
|
+
.map((x) => {
|
|
4543
|
+
const s = S.sessions.get(x.id);
|
|
4544
|
+
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>`;
|
|
4545
|
+
})
|
|
4546
|
+
.join('')}</ul>`
|
|
4547
|
+
: '<p class="mem-empty">Nothing yet. Launch with Auto and its picks show up here.</p>'}
|
|
4548
|
+
<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>`;
|
|
4549
|
+
$('#roReset', node).onclick = async () => {
|
|
4550
|
+
if (!confirm('Forget what Auto learned from your tasks?')) return;
|
|
4551
|
+
await api('/router/reset', { method: 'POST', body: {} }).catch((err) => toast(esc(err.message), { tone: 'bad' }));
|
|
4552
|
+
render();
|
|
4553
|
+
};
|
|
4554
|
+
};
|
|
4555
|
+
node.refresh = render;
|
|
4556
|
+
render();
|
|
4557
|
+
return close;
|
|
4558
|
+
}
|
|
4559
|
+
|
|
4424
4560
|
// --- memory: what the background retros learned ---------------------------------------------
|
|
4425
4561
|
|
|
4426
4562
|
const MEM_KINDS = [
|
|
@@ -4904,6 +5040,13 @@ function connect() {
|
|
|
4904
5040
|
es.addEventListener('remote', () => $('.phone-modal')?.refresh());
|
|
4905
5041
|
es.addEventListener('machines', (e) => setMachines(JSON.parse(e.data)));
|
|
4906
5042
|
es.addEventListener('update', (e) => renderUpdate(JSON.parse(e.data)));
|
|
5043
|
+
// A new harness or model Auto can use: it gets a few tasks before Auto relies on it.
|
|
5044
|
+
es.addEventListener('router', (e) => {
|
|
5045
|
+
const { added = [] } = JSON.parse(e.data || '{}');
|
|
5046
|
+
$('.router-modal')?.refresh();
|
|
5047
|
+
$('.settings-modal')?.paintAuto?.();
|
|
5048
|
+
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 });
|
|
5049
|
+
});
|
|
4907
5050
|
es.addEventListener('preferences', () => {
|
|
4908
5051
|
$('.memory-modal')?.refresh();
|
|
4909
5052
|
$('.settings-modal')?.paintLearn?.();
|