@nxisai/agent-harness 0.7.0 → 0.8.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/LICENSE +201 -0
- package/README.md +17 -5
- package/dist/harness/harness.d.ts +16 -0
- package/dist/harness/harness.d.ts.map +1 -1
- package/dist/harness/harness.js +44 -2
- package/dist/harness/harness.js.map +1 -1
- package/dist/harness/recommend.d.ts +46 -0
- package/dist/harness/recommend.d.ts.map +1 -0
- package/dist/harness/recommend.js +144 -0
- package/dist/harness/recommend.js.map +1 -0
- package/dist/harness/records.d.ts +70 -0
- package/dist/harness/records.d.ts.map +1 -0
- package/dist/harness/records.js +209 -0
- package/dist/harness/records.js.map +1 -0
- package/dist/harness/renderer.d.ts +11 -1
- package/dist/harness/renderer.d.ts.map +1 -1
- package/dist/harness/tools/action-tools.d.ts +10 -0
- package/dist/harness/tools/action-tools.d.ts.map +1 -1
- package/dist/harness/tools/action-tools.js +18 -0
- package/dist/harness/tools/action-tools.js.map +1 -1
- package/dist/harness/tools/site-pages.js +1 -1
- package/dist/index.d.ts +3 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -1
- package/dist/ui/agent-fullscreen.d.ts.map +1 -1
- package/dist/ui/agent-fullscreen.js +85 -73
- package/dist/ui/agent-fullscreen.js.map +1 -1
- package/dist/ui/result-pick.d.ts +52 -0
- package/dist/ui/result-pick.d.ts.map +1 -0
- package/dist/ui/result-pick.js +196 -0
- package/dist/ui/result-pick.js.map +1 -0
- package/package.json +1 -1
- package/src/harness/harness.ts +51 -2
- package/src/harness/recommend.ts +194 -0
- package/src/harness/records.ts +237 -0
- package/src/harness/renderer.ts +12 -1
- package/src/harness/tools/action-tools.ts +18 -0
- package/src/harness/tools/site-pages.ts +1 -1
- package/src/index.ts +3 -0
- package/src/ui/agent-fullscreen.ts +92 -63
- package/src/ui/result-pick.ts +233 -0
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Search results as an answer: a recommended pick with its reasons, the
|
|
3
|
+
* others to compare against, or, when there is nothing to recommend, a short
|
|
4
|
+
* list that opens up per result.
|
|
5
|
+
*
|
|
6
|
+
* The pick comes from the harness (`recommend`): the model chose it and wrote
|
|
7
|
+
* the lead and reasons, and only reasons the record backs survived. Those are
|
|
8
|
+
* the only lines marked with a tick. Comparing another result shows that
|
|
9
|
+
* result's own facts, plainly, without a tick: the agent did not recommend it.
|
|
10
|
+
*
|
|
11
|
+
* Plain DOM, every string set as text.
|
|
12
|
+
*
|
|
13
|
+
* @module
|
|
14
|
+
*/
|
|
15
|
+
import type { ResultPick } from '../harness/recommend.js';
|
|
16
|
+
/** One result, as the answer shows it. */
|
|
17
|
+
export interface AnswerCard {
|
|
18
|
+
title: string;
|
|
19
|
+
stars?: number;
|
|
20
|
+
reviews?: number;
|
|
21
|
+
status?: {
|
|
22
|
+
open: boolean;
|
|
23
|
+
label: string;
|
|
24
|
+
};
|
|
25
|
+
phone?: string;
|
|
26
|
+
page?: {
|
|
27
|
+
url: string;
|
|
28
|
+
label: string;
|
|
29
|
+
};
|
|
30
|
+
/** What the record states, short: hours, rating, services, since when. */
|
|
31
|
+
facts: string[];
|
|
32
|
+
/** A site form that can be sent to it, and what to ask the agent to start it. */
|
|
33
|
+
form?: {
|
|
34
|
+
label: string;
|
|
35
|
+
message: string;
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
/** What the answer can ask the conversation to do. */
|
|
39
|
+
export interface AnswerHandlers {
|
|
40
|
+
/** Send a message as the visitor: "Get a quote from Hatzel & Sons Plumbing". */
|
|
41
|
+
ask(text: string): void;
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Draw the answer.
|
|
45
|
+
*
|
|
46
|
+
* @param cards - The results, in order.
|
|
47
|
+
* @param pick - The recommendation, when the harness made one.
|
|
48
|
+
*/
|
|
49
|
+
export declare function resultsAnswer(cards: AnswerCard[], pick: ResultPick | undefined, handlers: AnswerHandlers): HTMLElement;
|
|
50
|
+
/** Styles, on the widget's tokens. */
|
|
51
|
+
export declare const RESULTS_ANSWER_CSS = "\n .results-answer { align-self: flex-start; width: min(100%, 440px); box-sizing: border-box; padding: 12px 14px; background: var(--surface); border: 1px solid var(--border); border-radius: 16px 16px 16px 4px; font-size: 14px; line-height: 1.5; color: var(--ink); white-space: normal; }\n .ra-body { display: flex; flex-direction: column; gap: 12px; }\n .ra-lead { margin: 0; }\n .ra-pick { background: var(--bg); border: 1px solid var(--border-strong); border-radius: 14px; padding: 12px; display: flex; flex-direction: column; gap: 10px; }\n .ra-pick.is-pick { border: 1.5px solid var(--blue); box-shadow: 0 0 0 4px var(--blue-tint); }\n .ra-head { display: flex; align-items: center; gap: 10px; }\n .ra-avatar { flex-shrink: 0; display: grid; place-items: center; color: #fff; font-weight: 700; letter-spacing: .02em; }\n .ra-avatar.lg { width: 40px; height: 40px; border-radius: 12px; font-size: 13px; }\n .ra-avatar.sm { width: 30px; height: 30px; border-radius: 9px; font-size: 11px; }\n .ra-who { display: flex; flex-direction: column; gap: 1px; min-width: 0; flex-grow: 1; text-align: left; }\n .ra-who strong { font-size: 14.5px; font-weight: 650; }\n .ra-meta { font-size: 12.5px; color: var(--ink-2); }\n .ra-meta strong { color: var(--ink); font-size: 12.5px; font-weight: 600; }\n .ra-faint { color: var(--ink-3); }\n .ra-open { color: var(--green-strong); font-weight: 600; }\n .ra-reasons { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 6px; font-size: 13px; color: var(--ink-2); }\n .ra-reasons li { display: flex; gap: 8px; align-items: flex-start; }\n .ra-mark { width: 18px; height: 18px; flex-shrink: 0; border-radius: 50%; display: grid; place-items: center; font-size: 11px; font-weight: 700; color: var(--ink-3); }\n .ra-reasons.ticked .ra-mark { background: var(--green-tint); color: var(--green-strong); }\n .ra-actions { display: flex; flex-wrap: wrap; gap: 8px; }\n .ra-btn { min-height: 40px; padding: 0 14px; border-radius: var(--radius-sm); border: 1px solid var(--border-strong); background: var(--bg); color: var(--ink); font: 600 13.5px var(--sans); display: inline-flex; align-items: center; justify-content: center; text-decoration: none; cursor: pointer; }\n .ra-btn.primary { flex-grow: 1; border: 0; background: var(--blue); color: #fff; }\n .ra-btn.primary:hover { background: var(--blue-strong); }\n .ra-also { display: flex; flex-direction: column; gap: 6px; }\n .ra-eyebrow { font-family: var(--mono); font-size: 10.5px; font-weight: 500; letter-spacing: .1em; text-transform: uppercase; color: var(--ink-3); }\n .ra-row { display: flex; align-items: center; gap: 10px; width: 100%; padding: 8px 10px; border: 1px solid var(--border); border-radius: 10px; background: var(--bg); color: var(--ink); font: inherit; cursor: pointer; }\n .ra-row:hover { border-color: var(--border-strong); }\n .ra-row.plain { border: 0; border-radius: 0; padding: 10px 12px; }\n .ra-chev { color: var(--ink-3); font-size: 17px; line-height: 1; }\n .ra-link { align-self: flex-start; border: 0; background: none; padding: 0; font: 600 13px var(--sans); color: var(--blue-strong); cursor: pointer; }\n .ra-list { background: var(--bg); border: 1px solid var(--border); border-radius: 12px; overflow: hidden; }\n .ra-item + .ra-item { border-top: 1px solid var(--border); }\n .ra-more { padding: 0 12px 12px 52px; display: flex; flex-direction: column; gap: 8px; }\n .ra-showmore { width: 100%; min-height: 40px; border: 0; border-top: 1px solid var(--border); background: var(--bg); color: var(--blue-strong); font: 600 13px var(--sans); cursor: pointer; }\n .ra-btn:focus-visible, .ra-row:focus-visible, .ra-link:focus-visible, .ra-showmore:focus-visible { outline: 2px solid var(--blue); outline-offset: 2px; }\n";
|
|
52
|
+
//# sourceMappingURL=result-pick.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"result-pick.d.ts","sourceRoot":"","sources":["../../src/ui/result-pick.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AACH,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,yBAAyB,CAAC;AAE1D,0CAA0C;AAC1C,MAAM,WAAW,UAAU;IACzB,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,MAAM,CAAC,EAAE;QAAE,IAAI,EAAE,OAAO,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE,CAAC;IAC1C,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,IAAI,CAAC,EAAE;QAAE,GAAG,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE,CAAC;IACtC,0EAA0E;IAC1E,KAAK,EAAE,MAAM,EAAE,CAAC;IAChB,iFAAiF;IACjF,IAAI,CAAC,EAAE;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,MAAM,CAAA;KAAE,CAAC;CAC3C;AAED,sDAAsD;AACtD,MAAM,WAAW,cAAc;IAC7B,gFAAgF;IAChF,GAAG,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;CACzB;AAuDD;;;;;GAKG;AACH,wBAAgB,aAAa,CAAC,KAAK,EAAE,UAAU,EAAE,EAAE,IAAI,EAAE,UAAU,GAAG,SAAS,EAAE,QAAQ,EAAE,cAAc,GAAG,WAAW,CAuDtH;AA6CD,sCAAsC;AACtC,eAAO,MAAM,kBAAkB,i3HAoC9B,CAAC"}
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
function h(tag, attrs = {}, ...children) {
|
|
2
|
+
const el = document.createElement(tag);
|
|
3
|
+
for (const [k, v] of Object.entries(attrs)) {
|
|
4
|
+
if (v === undefined)
|
|
5
|
+
continue;
|
|
6
|
+
if (k === 'class')
|
|
7
|
+
el.className = v;
|
|
8
|
+
else
|
|
9
|
+
el.setAttribute(k, v);
|
|
10
|
+
}
|
|
11
|
+
for (const c of children)
|
|
12
|
+
if (c)
|
|
13
|
+
el.append(c);
|
|
14
|
+
return el;
|
|
15
|
+
}
|
|
16
|
+
const PALETTE = ['#0E4F5C', '#1D4ED8', '#6D28D9', '#9A3412', '#0F766E', '#B45309', '#334155'];
|
|
17
|
+
/** Up to two letters, and a colour that stays the same for a name. */
|
|
18
|
+
function avatar(name, size) {
|
|
19
|
+
const words = name.split(/\s+/).filter((w) => /^[\p{L}\p{N}]/u.test(w));
|
|
20
|
+
const ini = (words.slice(0, 2).map((w) => w[0]).join('') || '?').toUpperCase();
|
|
21
|
+
let hash = 0;
|
|
22
|
+
for (const ch of name)
|
|
23
|
+
hash = (hash * 31 + ch.charCodeAt(0)) >>> 0;
|
|
24
|
+
const el = h('span', { class: `ra-avatar ${size}`, 'aria-hidden': 'true' }, ini);
|
|
25
|
+
el.style.background = PALETTE[hash % PALETTE.length];
|
|
26
|
+
return el;
|
|
27
|
+
}
|
|
28
|
+
/** "★ 4.7 (412) · Open now · 24/7". */
|
|
29
|
+
function meta(c) {
|
|
30
|
+
const line = h('span', { class: 'ra-meta' });
|
|
31
|
+
if (c.stars !== undefined) {
|
|
32
|
+
line.append(h('span', {}, `★ `, h('strong', {}, String(c.stars))));
|
|
33
|
+
if (c.reviews !== undefined)
|
|
34
|
+
line.append(h('span', { class: 'ra-faint' }, ` (${c.reviews})`));
|
|
35
|
+
}
|
|
36
|
+
if (c.status) {
|
|
37
|
+
if (line.childNodes.length)
|
|
38
|
+
line.append(' · ');
|
|
39
|
+
line.append(h('span', { class: c.status.open ? 'ra-open' : '' }, c.status.label));
|
|
40
|
+
}
|
|
41
|
+
return line;
|
|
42
|
+
}
|
|
43
|
+
function actions(c, handlers, primary) {
|
|
44
|
+
const row = h('div', { class: 'ra-actions' });
|
|
45
|
+
const tel = c.phone ? c.phone.replace(/[^\d+]/g, '') : '';
|
|
46
|
+
if (tel)
|
|
47
|
+
row.append(h('a', { class: `ra-btn ${primary ? 'primary' : ''}`, href: `tel:${tel}` }, `Call ${c.phone}`));
|
|
48
|
+
if (c.form) {
|
|
49
|
+
const b = h('button', { type: 'button', class: `ra-btn ${primary && !tel ? 'primary' : ''}` }, c.form.label);
|
|
50
|
+
b.addEventListener('click', () => handlers.ask(c.form.message));
|
|
51
|
+
row.append(b);
|
|
52
|
+
}
|
|
53
|
+
if (c.page)
|
|
54
|
+
row.append(h('a', { class: 'ra-btn', href: c.page.url }, 'Their page'));
|
|
55
|
+
return row;
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Draw the answer.
|
|
59
|
+
*
|
|
60
|
+
* @param cards - The results, in order.
|
|
61
|
+
* @param pick - The recommendation, when the harness made one.
|
|
62
|
+
*/
|
|
63
|
+
export function resultsAnswer(cards, pick, handlers) {
|
|
64
|
+
const wrap = h('div', { class: 'msg assistant results-answer' });
|
|
65
|
+
const valid = pick && cards[pick.index] ? pick : undefined;
|
|
66
|
+
if (!valid) {
|
|
67
|
+
wrap.append(list(cards, handlers, 0));
|
|
68
|
+
return wrap;
|
|
69
|
+
}
|
|
70
|
+
let showing = valid.index;
|
|
71
|
+
const body = h('div', { class: 'ra-body' });
|
|
72
|
+
const draw = () => {
|
|
73
|
+
const c = cards[showing];
|
|
74
|
+
const mine = showing === valid.index;
|
|
75
|
+
const box = h('div', { class: `ra-pick ${mine ? 'is-pick' : ''}` });
|
|
76
|
+
const head = h('div', { class: 'ra-head' }, avatar(c.title, 'lg'), h('div', { class: 'ra-who' }, h('strong', {}, c.title), meta(c)));
|
|
77
|
+
box.append(head);
|
|
78
|
+
// Ticks only for what the agent recommended, and could back from the record.
|
|
79
|
+
const lines = mine ? valid.reasons : c.facts;
|
|
80
|
+
if (lines.length) {
|
|
81
|
+
const ul = h('ul', { class: `ra-reasons ${mine ? 'ticked' : ''}` });
|
|
82
|
+
lines.forEach((l) => ul.append(h('li', {}, h('span', { class: 'ra-mark', 'aria-hidden': 'true' }, mine ? '✓' : '•'), h('span', {}, l))));
|
|
83
|
+
box.append(ul);
|
|
84
|
+
}
|
|
85
|
+
box.append(actions(c, handlers, true));
|
|
86
|
+
const lead = mine ? h('p', { class: 'ra-lead' }, valid.lead) : h('p', { class: 'ra-lead' }, `Comparing ${c.title} with my pick, ${cards[valid.index].title}. `);
|
|
87
|
+
if (!mine) {
|
|
88
|
+
const back = h('button', { type: 'button', class: 'ra-link' }, 'Back to my pick');
|
|
89
|
+
back.addEventListener('click', () => {
|
|
90
|
+
showing = valid.index;
|
|
91
|
+
draw();
|
|
92
|
+
});
|
|
93
|
+
lead.append(back);
|
|
94
|
+
}
|
|
95
|
+
const others = cards.map((o, i) => ({ o, i })).filter(({ i }) => i !== showing).slice(0, 3);
|
|
96
|
+
const also = h('div', { class: 'ra-also' }, h('div', { class: 'ra-eyebrow' }, mine ? 'Also good · tap to compare' : 'Others'));
|
|
97
|
+
for (const { o, i } of others) {
|
|
98
|
+
const row = h('button', { type: 'button', class: 'ra-row' }, avatar(o.title, 'sm'), h('span', { class: 'ra-who' }, h('strong', {}, o.title), meta(o)), h('span', { class: 'ra-chev', 'aria-hidden': 'true' }, '›'));
|
|
99
|
+
row.addEventListener('click', () => {
|
|
100
|
+
showing = i;
|
|
101
|
+
draw();
|
|
102
|
+
});
|
|
103
|
+
also.append(row);
|
|
104
|
+
}
|
|
105
|
+
body.replaceChildren(lead, box, ...(others.length ? [also] : []));
|
|
106
|
+
if (cards.length > 4) {
|
|
107
|
+
const all = h('button', { type: 'button', class: 'ra-link' }, `Show all ${cards.length}`);
|
|
108
|
+
all.addEventListener('click', () => body.replaceChildren(list(cards, handlers, valid.index)));
|
|
109
|
+
body.append(all);
|
|
110
|
+
}
|
|
111
|
+
};
|
|
112
|
+
draw();
|
|
113
|
+
wrap.append(body);
|
|
114
|
+
return wrap;
|
|
115
|
+
}
|
|
116
|
+
/** The plain list: one line per result, each opening to its facts and actions. */
|
|
117
|
+
function list(cards, handlers, open) {
|
|
118
|
+
const box = h('div', { class: 'ra-list' });
|
|
119
|
+
const SHOWN = 5;
|
|
120
|
+
let all = cards.length <= SHOWN + 1;
|
|
121
|
+
let openIndex = open;
|
|
122
|
+
const draw = () => {
|
|
123
|
+
box.replaceChildren();
|
|
124
|
+
cards.slice(0, all ? cards.length : SHOWN).forEach((c, i) => {
|
|
125
|
+
const expanded = openIndex === i;
|
|
126
|
+
const item = h('div', { class: 'ra-item' });
|
|
127
|
+
const id = `ra-${i}-${Math.random().toString(36).slice(2, 7)}`;
|
|
128
|
+
const row = h('button', { type: 'button', class: 'ra-row plain', 'aria-expanded': String(expanded), 'aria-controls': id }, avatar(c.title, 'sm'), h('span', { class: 'ra-who' }, h('strong', {}, c.title), meta(c)), h('span', { class: 'ra-chev', 'aria-hidden': 'true' }, expanded ? '−' : '+'));
|
|
129
|
+
row.addEventListener('click', () => {
|
|
130
|
+
openIndex = expanded ? null : i;
|
|
131
|
+
draw();
|
|
132
|
+
});
|
|
133
|
+
item.append(row);
|
|
134
|
+
if (expanded) {
|
|
135
|
+
const more = h('div', { class: 'ra-more', id });
|
|
136
|
+
if (c.facts.length) {
|
|
137
|
+
const ul = h('ul', { class: 'ra-reasons' });
|
|
138
|
+
c.facts.forEach((f) => ul.append(h('li', {}, h('span', { class: 'ra-mark', 'aria-hidden': 'true' }, '•'), h('span', {}, f))));
|
|
139
|
+
more.append(ul);
|
|
140
|
+
}
|
|
141
|
+
more.append(actions(c, handlers, false));
|
|
142
|
+
item.append(more);
|
|
143
|
+
}
|
|
144
|
+
box.append(item);
|
|
145
|
+
});
|
|
146
|
+
if (!all) {
|
|
147
|
+
const more = h('button', { type: 'button', class: 'ra-showmore' }, `Show ${cards.length - SHOWN} more`);
|
|
148
|
+
more.addEventListener('click', () => {
|
|
149
|
+
all = true;
|
|
150
|
+
draw();
|
|
151
|
+
});
|
|
152
|
+
box.append(more);
|
|
153
|
+
}
|
|
154
|
+
};
|
|
155
|
+
draw();
|
|
156
|
+
return box;
|
|
157
|
+
}
|
|
158
|
+
/** Styles, on the widget's tokens. */
|
|
159
|
+
export const RESULTS_ANSWER_CSS = `
|
|
160
|
+
.results-answer { align-self: flex-start; width: min(100%, 440px); box-sizing: border-box; padding: 12px 14px; background: var(--surface); border: 1px solid var(--border); border-radius: 16px 16px 16px 4px; font-size: 14px; line-height: 1.5; color: var(--ink); white-space: normal; }
|
|
161
|
+
.ra-body { display: flex; flex-direction: column; gap: 12px; }
|
|
162
|
+
.ra-lead { margin: 0; }
|
|
163
|
+
.ra-pick { background: var(--bg); border: 1px solid var(--border-strong); border-radius: 14px; padding: 12px; display: flex; flex-direction: column; gap: 10px; }
|
|
164
|
+
.ra-pick.is-pick { border: 1.5px solid var(--blue); box-shadow: 0 0 0 4px var(--blue-tint); }
|
|
165
|
+
.ra-head { display: flex; align-items: center; gap: 10px; }
|
|
166
|
+
.ra-avatar { flex-shrink: 0; display: grid; place-items: center; color: #fff; font-weight: 700; letter-spacing: .02em; }
|
|
167
|
+
.ra-avatar.lg { width: 40px; height: 40px; border-radius: 12px; font-size: 13px; }
|
|
168
|
+
.ra-avatar.sm { width: 30px; height: 30px; border-radius: 9px; font-size: 11px; }
|
|
169
|
+
.ra-who { display: flex; flex-direction: column; gap: 1px; min-width: 0; flex-grow: 1; text-align: left; }
|
|
170
|
+
.ra-who strong { font-size: 14.5px; font-weight: 650; }
|
|
171
|
+
.ra-meta { font-size: 12.5px; color: var(--ink-2); }
|
|
172
|
+
.ra-meta strong { color: var(--ink); font-size: 12.5px; font-weight: 600; }
|
|
173
|
+
.ra-faint { color: var(--ink-3); }
|
|
174
|
+
.ra-open { color: var(--green-strong); font-weight: 600; }
|
|
175
|
+
.ra-reasons { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 6px; font-size: 13px; color: var(--ink-2); }
|
|
176
|
+
.ra-reasons li { display: flex; gap: 8px; align-items: flex-start; }
|
|
177
|
+
.ra-mark { width: 18px; height: 18px; flex-shrink: 0; border-radius: 50%; display: grid; place-items: center; font-size: 11px; font-weight: 700; color: var(--ink-3); }
|
|
178
|
+
.ra-reasons.ticked .ra-mark { background: var(--green-tint); color: var(--green-strong); }
|
|
179
|
+
.ra-actions { display: flex; flex-wrap: wrap; gap: 8px; }
|
|
180
|
+
.ra-btn { min-height: 40px; padding: 0 14px; border-radius: var(--radius-sm); border: 1px solid var(--border-strong); background: var(--bg); color: var(--ink); font: 600 13.5px var(--sans); display: inline-flex; align-items: center; justify-content: center; text-decoration: none; cursor: pointer; }
|
|
181
|
+
.ra-btn.primary { flex-grow: 1; border: 0; background: var(--blue); color: #fff; }
|
|
182
|
+
.ra-btn.primary:hover { background: var(--blue-strong); }
|
|
183
|
+
.ra-also { display: flex; flex-direction: column; gap: 6px; }
|
|
184
|
+
.ra-eyebrow { font-family: var(--mono); font-size: 10.5px; font-weight: 500; letter-spacing: .1em; text-transform: uppercase; color: var(--ink-3); }
|
|
185
|
+
.ra-row { display: flex; align-items: center; gap: 10px; width: 100%; padding: 8px 10px; border: 1px solid var(--border); border-radius: 10px; background: var(--bg); color: var(--ink); font: inherit; cursor: pointer; }
|
|
186
|
+
.ra-row:hover { border-color: var(--border-strong); }
|
|
187
|
+
.ra-row.plain { border: 0; border-radius: 0; padding: 10px 12px; }
|
|
188
|
+
.ra-chev { color: var(--ink-3); font-size: 17px; line-height: 1; }
|
|
189
|
+
.ra-link { align-self: flex-start; border: 0; background: none; padding: 0; font: 600 13px var(--sans); color: var(--blue-strong); cursor: pointer; }
|
|
190
|
+
.ra-list { background: var(--bg); border: 1px solid var(--border); border-radius: 12px; overflow: hidden; }
|
|
191
|
+
.ra-item + .ra-item { border-top: 1px solid var(--border); }
|
|
192
|
+
.ra-more { padding: 0 12px 12px 52px; display: flex; flex-direction: column; gap: 8px; }
|
|
193
|
+
.ra-showmore { width: 100%; min-height: 40px; border: 0; border-top: 1px solid var(--border); background: var(--bg); color: var(--blue-strong); font: 600 13px var(--sans); cursor: pointer; }
|
|
194
|
+
.ra-btn:focus-visible, .ra-row:focus-visible, .ra-link:focus-visible, .ra-showmore:focus-visible { outline: 2px solid var(--blue); outline-offset: 2px; }
|
|
195
|
+
`;
|
|
196
|
+
//# sourceMappingURL=result-pick.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"result-pick.js","sourceRoot":"","sources":["../../src/ui/result-pick.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nxisai/agent-harness",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.8.0",
|
|
4
4
|
"description": "An on-device agent for the browser: a tool-calling harness over WebMCP and remote MCP, plus a drop-in chat UI. Decides whether to run a tool, ask for what's missing, or confirm before writing.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
package/src/harness/harness.ts
CHANGED
|
@@ -39,6 +39,8 @@ import { decide, policyFor, type Decision } from './policy.js';
|
|
|
39
39
|
import { buildPlan } from './plan.js';
|
|
40
40
|
import { resolveSlots, summarySlots, type Resolution } from './slots.js';
|
|
41
41
|
import { beginJourney, journeySteps, recordStep, type AgentStep } from './steps.js';
|
|
42
|
+
import { extractRecords } from './records.js';
|
|
43
|
+
import { recommend, type ResultPick } from './recommend.js';
|
|
42
44
|
import { HANDOFF_SESSION_RESULT, TALK_TO_A_PERSON_TOOL, isHandoffTool } from '../handoff/tools.js';
|
|
43
45
|
import type { HarnessRenderer, StreamingBubble } from './renderer.js';
|
|
44
46
|
|
|
@@ -111,6 +113,15 @@ export interface HarnessOptions {
|
|
|
111
113
|
* Defaults to true. Pass false to ask one question at a time.
|
|
112
114
|
*/
|
|
113
115
|
groupRelated?: boolean;
|
|
116
|
+
/**
|
|
117
|
+
* Whether a search that finds several records ends with a recommendation:
|
|
118
|
+
* the model picks one and gives reasons, and only reasons the record backs
|
|
119
|
+
* are kept (see `recommend`). Defaults to true. Costs one more completion
|
|
120
|
+
* per search.
|
|
121
|
+
*/
|
|
122
|
+
recommend?: boolean;
|
|
123
|
+
/** The clock "open now" is judged by. For tests. */
|
|
124
|
+
now?: () => Date;
|
|
114
125
|
/** Called whenever the conversation changes, for hosts that persist it. */
|
|
115
126
|
onMessages?(messages: ChatMessage[]): void;
|
|
116
127
|
}
|
|
@@ -323,6 +334,8 @@ export class AgentHarness {
|
|
|
323
334
|
/** The previous request's system prompt and tool names, for the cache hint. */
|
|
324
335
|
private lastPrompt = '';
|
|
325
336
|
private lastTools = '';
|
|
337
|
+
/** What the visitor asked this turn, for a recommendation to answer. */
|
|
338
|
+
private goal = '';
|
|
326
339
|
|
|
327
340
|
constructor(opts: HarnessOptions) {
|
|
328
341
|
this.provider = opts.provider ?? new LocalGemmaProvider();
|
|
@@ -434,6 +447,7 @@ export class AgentHarness {
|
|
|
434
447
|
// rather than the previous turn's.
|
|
435
448
|
this.abort?.abort();
|
|
436
449
|
this.abort = new AbortController();
|
|
450
|
+
this.goal = goal;
|
|
437
451
|
this.renderer.stoppable?.(true);
|
|
438
452
|
|
|
439
453
|
// Opened before the model is touched: loading weights is the longest
|
|
@@ -834,6 +848,35 @@ export class AgentHarness {
|
|
|
834
848
|
this.renderer.step?.(step, journeySteps());
|
|
835
849
|
}
|
|
836
850
|
|
|
851
|
+
/**
|
|
852
|
+
* The record to recommend from a search's results, when there are several
|
|
853
|
+
* to choose between and the model can say why from what they state.
|
|
854
|
+
*/
|
|
855
|
+
private async pickFrom(res: ModelContextToolResponse): Promise<{ pick: ResultPick; name: string } | null> {
|
|
856
|
+
if (this.opts.recommend === false || !this.goal) return null;
|
|
857
|
+
// Records of things, not rows of a type listing: something with a name and a kind.
|
|
858
|
+
const records = extractRecords(res).filter((r) => typeof r.name === 'string' && r['@type'] != null);
|
|
859
|
+
if (records.length < 2) return null;
|
|
860
|
+
const bubble = this.renderer.streaming();
|
|
861
|
+
bubble.thinking('Comparing them…');
|
|
862
|
+
try {
|
|
863
|
+
const pick = await recommend({
|
|
864
|
+
provider: this.provider,
|
|
865
|
+
goal: this.goal,
|
|
866
|
+
records,
|
|
867
|
+
now: this.opts.now?.(),
|
|
868
|
+
signal: this.abort?.signal,
|
|
869
|
+
transcribe: withTranscribedCall,
|
|
870
|
+
});
|
|
871
|
+
return pick ? { pick, name: String(records[pick.index].name) } : null;
|
|
872
|
+
} catch {
|
|
873
|
+
// Stopped, or the model failed: the results stand on their own.
|
|
874
|
+
return null;
|
|
875
|
+
} finally {
|
|
876
|
+
bubble.finalize('');
|
|
877
|
+
}
|
|
878
|
+
}
|
|
879
|
+
|
|
837
880
|
/** Discard a live prompt and the values typed into it. */
|
|
838
881
|
private cancel(): void {
|
|
839
882
|
// A journey that stops here should say so, not sit waiting for a press
|
|
@@ -974,8 +1017,14 @@ export class AgentHarness {
|
|
|
974
1017
|
});
|
|
975
1018
|
|
|
976
1019
|
if (readOnly) {
|
|
977
|
-
this.
|
|
978
|
-
|
|
1020
|
+
const picked = await this.pickFrom(res);
|
|
1021
|
+
this.renderer.results(tool, res, picked ? { pick: picked.pick } : undefined);
|
|
1022
|
+
messages.push({
|
|
1023
|
+
role: 'user',
|
|
1024
|
+
content:
|
|
1025
|
+
`[${tool.name} returned: ${text.slice(0, 400)}]` +
|
|
1026
|
+
(picked ? ` [You recommended ${picked.name}: ${picked.pick.reasons.join(' ')}]` : ''),
|
|
1027
|
+
});
|
|
979
1028
|
} else {
|
|
980
1029
|
this.renderer.detail(tool, values, text);
|
|
981
1030
|
messages.push({
|
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Recommending one result, with reasons the records support.
|
|
3
|
+
*
|
|
4
|
+
* After a search returns several records, the visitor's next question is
|
|
5
|
+
* nearly always "which one?". The model answers it in one more completion:
|
|
6
|
+
* it picks a record and gives a short lead and up to three reasons. Then the
|
|
7
|
+
* harness checks every reason against the facts of the record it picked. A
|
|
8
|
+
* number the record does not state, or a reason that shares no words with
|
|
9
|
+
* it, is dropped. A pick with no reason left is no pick at all, and the
|
|
10
|
+
* renderer shows the plain list instead.
|
|
11
|
+
*
|
|
12
|
+
* The model chooses and words it; the records decide what may be said.
|
|
13
|
+
*
|
|
14
|
+
* @module
|
|
15
|
+
*/
|
|
16
|
+
import type { ChatMessage } from '../engine.js';
|
|
17
|
+
import type { Completion, ModelProvider } from '../providers/types.js';
|
|
18
|
+
import type { ModelContextToolInfo } from '../types.js';
|
|
19
|
+
import { factLines, recordFacts, type RecordFacts } from './records.js';
|
|
20
|
+
|
|
21
|
+
/** The tool the model answers through. Never registered anywhere: offered for this one completion. */
|
|
22
|
+
export const RECOMMEND_TOOL = 'recommend';
|
|
23
|
+
|
|
24
|
+
/** A recommendation the renderer can show. */
|
|
25
|
+
export interface ResultPick {
|
|
26
|
+
/** The picked record's position in the results, from 0. */
|
|
27
|
+
index: number;
|
|
28
|
+
/** Its key: `@id`, else its name. */
|
|
29
|
+
key: string;
|
|
30
|
+
/** One sentence for the visitor, naming it. */
|
|
31
|
+
lead: string;
|
|
32
|
+
/** Up to three reasons, each backed by the record. */
|
|
33
|
+
reasons: string[];
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** What {@link recommend} needs. */
|
|
37
|
+
export interface RecommendRequest {
|
|
38
|
+
provider: ModelProvider;
|
|
39
|
+
/** What the visitor asked. */
|
|
40
|
+
goal: string;
|
|
41
|
+
/** The records, in the order the results show them. */
|
|
42
|
+
records: Record<string, unknown>[];
|
|
43
|
+
/** The time "open now" is judged at. Defaults to now. */
|
|
44
|
+
now?: Date;
|
|
45
|
+
signal?: AbortSignal;
|
|
46
|
+
/** Reads a call the model wrote out as text. */
|
|
47
|
+
transcribe?: (c: Completion) => Completion;
|
|
48
|
+
/** How many records the model compares. Defaults to eight. */
|
|
49
|
+
limit?: number;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const TOOL: ModelContextToolInfo = {
|
|
53
|
+
name: RECOMMEND_TOOL,
|
|
54
|
+
description: 'Recommend the one result that best fits what the visitor asked, with reasons taken from that result.',
|
|
55
|
+
inputSchema: {
|
|
56
|
+
type: 'object',
|
|
57
|
+
properties: {
|
|
58
|
+
pick: { type: 'integer', description: 'The number of the result you recommend.' },
|
|
59
|
+
lead: { type: 'string', description: 'One short sentence for the visitor, naming it and saying why.' },
|
|
60
|
+
reasons: {
|
|
61
|
+
type: 'array',
|
|
62
|
+
items: { type: 'string' },
|
|
63
|
+
description: 'Two or three short reasons, each restating a fact listed for that result.',
|
|
64
|
+
},
|
|
65
|
+
},
|
|
66
|
+
required: ['pick', 'lead', 'reasons'],
|
|
67
|
+
},
|
|
68
|
+
} as ModelContextToolInfo;
|
|
69
|
+
|
|
70
|
+
const WEEKDAY = new Intl.DateTimeFormat('en-US', { weekday: 'long', hour: 'numeric', minute: '2-digit' });
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Ask the model which result fits, and keep only what the record supports.
|
|
74
|
+
*
|
|
75
|
+
* @returns The pick, or null when the model gave none, picked a result that
|
|
76
|
+
* does not exist, or gave no reason the record backs.
|
|
77
|
+
*/
|
|
78
|
+
export async function recommend(req: RecommendRequest): Promise<ResultPick | null> {
|
|
79
|
+
const now = req.now ?? new Date();
|
|
80
|
+
const records = req.records.slice(0, req.limit ?? 8);
|
|
81
|
+
if (records.length < 2) return null;
|
|
82
|
+
const facts = records.map((r) => recordFacts(r, now));
|
|
83
|
+
const listing = facts.map((f, i) => `${i + 1}. ${f.name}\n${factLines(f).map((l) => ` - ${l}`).join('\n')}`).join('\n');
|
|
84
|
+
|
|
85
|
+
const messages: ChatMessage[] = [
|
|
86
|
+
{
|
|
87
|
+
role: 'system',
|
|
88
|
+
content: [
|
|
89
|
+
'You help a website visitor choose between search results.',
|
|
90
|
+
`Call ${RECOMMEND_TOOL} once: pick is the number of the result that best fits what they asked; lead is one short sentence that names it and says why; reasons are two or three short reasons.`,
|
|
91
|
+
'Every reason must restate a fact listed for the result you pick. Do not add facts, prices, times or numbers that are not listed.',
|
|
92
|
+
'If they need help now, prefer one that is open now.',
|
|
93
|
+
].join(' '),
|
|
94
|
+
},
|
|
95
|
+
{
|
|
96
|
+
role: 'user',
|
|
97
|
+
content: `The visitor asked: "${req.goal}"\nIt is now ${WEEKDAY.format(now)}.\n\nResults:\n${listing}`,
|
|
98
|
+
},
|
|
99
|
+
];
|
|
100
|
+
|
|
101
|
+
let completion: Completion;
|
|
102
|
+
try {
|
|
103
|
+
completion = await req.provider.complete({ messages, tools: [TOOL], signal: req.signal, maxTokens: 300 });
|
|
104
|
+
} catch (err) {
|
|
105
|
+
if (req.signal?.aborted) throw err;
|
|
106
|
+
console.warn('[nxis-agent] recommendation failed:', err);
|
|
107
|
+
return null;
|
|
108
|
+
}
|
|
109
|
+
if (req.transcribe) completion = req.transcribe(completion);
|
|
110
|
+
const args = completion.call?.name === RECOMMEND_TOOL ? completion.call.args : readJson(completion.text);
|
|
111
|
+
if (!args) return null;
|
|
112
|
+
|
|
113
|
+
const index = Number(args.pick) - 1;
|
|
114
|
+
const picked = facts[index];
|
|
115
|
+
if (!Number.isInteger(index) || !picked) return null;
|
|
116
|
+
const others = facts.filter((_, i) => i !== index).map((f) => f.name.toLowerCase());
|
|
117
|
+
const reasons = (Array.isArray(args.reasons) ? args.reasons : [])
|
|
118
|
+
.map((r) => String(r ?? '').trim())
|
|
119
|
+
.filter((r) => r && r.length <= 200 && grounded(r, picked, others))
|
|
120
|
+
.slice(0, 3);
|
|
121
|
+
if (!reasons.length) return null;
|
|
122
|
+
|
|
123
|
+
const said = String(args.lead ?? '').trim();
|
|
124
|
+
const lead = said && said.length <= 200 && mentions(said, picked.name) && grounded(said, picked, others) ? said : `I'd go with ${picked.name}.`;
|
|
125
|
+
return { index, key: picked.key, lead, reasons };
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/** A call written as plain JSON: `{"pick": 1, …}`. */
|
|
129
|
+
function readJson(text: string): Record<string, unknown> | null {
|
|
130
|
+
const m = text.match(/\{[\s\S]*\}/);
|
|
131
|
+
if (!m) return null;
|
|
132
|
+
try {
|
|
133
|
+
const v = JSON.parse(m[0]);
|
|
134
|
+
return v && typeof v === 'object' ? (v as Record<string, unknown>) : null;
|
|
135
|
+
} catch {
|
|
136
|
+
return null;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
const STOP = new Set(
|
|
141
|
+
'about after also and are around been best both but can come does done each even from good great have here into just like more most much need only open other over same some than that their them then there these they this those very well were what when where which while will with would your'.split(
|
|
142
|
+
' ',
|
|
143
|
+
),
|
|
144
|
+
);
|
|
145
|
+
|
|
146
|
+
/** The words worth matching: four letters or more, not filler, roughly stemmed. */
|
|
147
|
+
function words(text: string): string[] {
|
|
148
|
+
return (text.toLowerCase().match(/[a-z][a-z'-]{3,}/g) ?? []).filter((w) => !STOP.has(w)).map(stem);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
const stem = (w: string): string => w.replace(/'s$/, '').replace(/(ing|ers|ed|es|s)$/, '').slice(0, 7);
|
|
152
|
+
|
|
153
|
+
/** The record's facts as one searchable text. */
|
|
154
|
+
function factText(f: RecordFacts): string {
|
|
155
|
+
return [f.name, ...factLines(f)].join(' ').toLowerCase();
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Whether a sentence says only what the record says.
|
|
160
|
+
*
|
|
161
|
+
* Every number in it must appear in the record ("4.7", "412", "40", "24/7");
|
|
162
|
+
* it must share at least one meaningful word with the record; and it must
|
|
163
|
+
* not be about another result.
|
|
164
|
+
*/
|
|
165
|
+
export function grounded(sentence: string, f: RecordFacts, others: string[] = []): boolean {
|
|
166
|
+
const text = factText(f);
|
|
167
|
+
const lower = sentence.toLowerCase();
|
|
168
|
+
if (others.some((name) => name && lower.includes(name))) return false;
|
|
169
|
+
const numbers = lower.match(/\d+(?:[.:]\d+)?/g) ?? [];
|
|
170
|
+
const numberText = text.replace(/,/g, '');
|
|
171
|
+
if (!numbers.every((n) => numberText.includes(n))) return false;
|
|
172
|
+
// Hours are claims about now, checked against the hours rather than the words:
|
|
173
|
+
// every record says "open" somewhere, closed ones too.
|
|
174
|
+
const always = ALWAYS.test(lower);
|
|
175
|
+
const now = OPEN_NOW.test(lower);
|
|
176
|
+
if (always && !(f.status?.always || /24\/7|24 hours/.test(text))) return false;
|
|
177
|
+
if (now && !f.status?.open) return false;
|
|
178
|
+
if (always || now) return true;
|
|
179
|
+
const known = new Set(words(text));
|
|
180
|
+
return words(sentence).some((w) => known.has(w));
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/** A claim that it never closes. */
|
|
184
|
+
const ALWAYS = /24\/7|24 hours|around the clock|day (?:or|and) night|any ?time/;
|
|
185
|
+
/** A claim that it is open at this moment. */
|
|
186
|
+
const OPEN_NOW = /\bopen (?:right )?now\b|\bright now\b|\bopen today\b|\bstill open\b/;
|
|
187
|
+
|
|
188
|
+
/** Whether a sentence names the record: its full name, or its first distinctive word. */
|
|
189
|
+
function mentions(sentence: string, name: string): boolean {
|
|
190
|
+
const lower = sentence.toLowerCase();
|
|
191
|
+
if (lower.includes(name.toLowerCase())) return true;
|
|
192
|
+
const first = words(name).find((w) => w.length >= 4);
|
|
193
|
+
return !!first && words(sentence).includes(first);
|
|
194
|
+
}
|