@nxisai/agent-harness 0.7.0 → 0.9.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.
Files changed (62) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +17 -5
  3. package/dist/a2ui/cards.d.ts +3 -0
  4. package/dist/a2ui/cards.d.ts.map +1 -1
  5. package/dist/a2ui/cards.js +11 -2
  6. package/dist/a2ui/cards.js.map +1 -1
  7. package/dist/harness/cta.d.ts +27 -0
  8. package/dist/harness/cta.d.ts.map +1 -0
  9. package/dist/harness/cta.js +43 -0
  10. package/dist/harness/cta.js.map +1 -0
  11. package/dist/harness/harness.d.ts +33 -0
  12. package/dist/harness/harness.d.ts.map +1 -1
  13. package/dist/harness/harness.js +84 -2
  14. package/dist/harness/harness.js.map +1 -1
  15. package/dist/harness/recommend.d.ts +46 -0
  16. package/dist/harness/recommend.d.ts.map +1 -0
  17. package/dist/harness/recommend.js +144 -0
  18. package/dist/harness/recommend.js.map +1 -0
  19. package/dist/harness/records.d.ts +70 -0
  20. package/dist/harness/records.d.ts.map +1 -0
  21. package/dist/harness/records.js +209 -0
  22. package/dist/harness/records.js.map +1 -0
  23. package/dist/harness/renderer.d.ts +11 -1
  24. package/dist/harness/renderer.d.ts.map +1 -1
  25. package/dist/harness/router/coordinator.d.ts +6 -0
  26. package/dist/harness/router/coordinator.d.ts.map +1 -1
  27. package/dist/harness/router/coordinator.js +14 -0
  28. package/dist/harness/router/coordinator.js.map +1 -1
  29. package/dist/harness/tools/action-tools.d.ts +25 -0
  30. package/dist/harness/tools/action-tools.d.ts.map +1 -1
  31. package/dist/harness/tools/action-tools.js +39 -0
  32. package/dist/harness/tools/action-tools.js.map +1 -1
  33. package/dist/harness/tools/inbound.d.ts +88 -0
  34. package/dist/harness/tools/inbound.d.ts.map +1 -0
  35. package/dist/harness/tools/inbound.js +156 -0
  36. package/dist/harness/tools/inbound.js.map +1 -0
  37. package/dist/harness/tools/site-pages.js +1 -1
  38. package/dist/index.d.ts +7 -1
  39. package/dist/index.d.ts.map +1 -1
  40. package/dist/index.js +5 -0
  41. package/dist/index.js.map +1 -1
  42. package/dist/ui/agent-fullscreen.d.ts.map +1 -1
  43. package/dist/ui/agent-fullscreen.js +154 -76
  44. package/dist/ui/agent-fullscreen.js.map +1 -1
  45. package/dist/ui/result-pick.d.ts +52 -0
  46. package/dist/ui/result-pick.d.ts.map +1 -0
  47. package/dist/ui/result-pick.js +196 -0
  48. package/dist/ui/result-pick.js.map +1 -0
  49. package/package.json +1 -1
  50. package/src/a2ui/cards.ts +14 -2
  51. package/src/harness/cta.ts +54 -0
  52. package/src/harness/harness.ts +93 -2
  53. package/src/harness/recommend.ts +194 -0
  54. package/src/harness/records.ts +237 -0
  55. package/src/harness/renderer.ts +12 -1
  56. package/src/harness/router/coordinator.ts +14 -0
  57. package/src/harness/tools/action-tools.ts +41 -0
  58. package/src/harness/tools/inbound.ts +189 -0
  59. package/src/harness/tools/site-pages.ts +1 -1
  60. package/src/index.ts +16 -0
  61. package/src/ui/agent-fullscreen.ts +162 -66
  62. 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.7.0",
3
+ "version": "0.9.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/a2ui/cards.ts CHANGED
@@ -12,6 +12,7 @@
12
12
  */
13
13
 
14
14
  import type { A2uiMessage } from './emit.js';
15
+ import { actionCta, type CardAction } from '../harness/cta.js';
15
16
 
16
17
  const CATALOG = 'https://a2ui.org/specification/v1_0/catalogs/basic/catalog.json';
17
18
 
@@ -27,6 +28,8 @@ export interface ResultCardData {
27
28
  sub: string;
28
29
  tags: CardTag[];
29
30
  rows: CardRow[];
31
+ /** The record's own action, as at most one button. A list so a card without one draws nothing. */
32
+ actions: CardAction[];
30
33
  }
31
34
 
32
35
  const str = (v: unknown): string => (v == null ? '' : String(Array.isArray(v) ? v[0] : v));
@@ -93,7 +96,8 @@ export function toCardData(entity: Record<string, unknown>): ResultCardData {
93
96
  }
94
97
  }
95
98
 
96
- return { title, sub, tags, rows };
99
+ const action = actionCta(entity, title);
100
+ return { title, sub, tags, rows, actions: action ? [action] : [] };
97
101
  }
98
102
 
99
103
  /** Fixed component set — independent of how many results there are. */
@@ -106,7 +110,7 @@ function cardComponents(heading: string): Component[] {
106
110
  { id: 'results', component: 'List',
107
111
  children: { path: '/results', componentId: 'card_tpl' }, direction: 'vertical' },
108
112
  { id: 'card_tpl', component: 'Card', child: 'card_body' },
109
- { id: 'card_body', component: 'Column', children: ['card_title', 'card_tags', 'card_sub', 'card_rows'] },
113
+ { id: 'card_body', component: 'Column', children: ['card_title', 'card_tags', 'card_sub', 'card_rows', 'card_actions'] },
110
114
  // No bold: `weight` in this catalog is a flex weight (number), and Text
111
115
  // offers only variant caption/body. Title emphasis is not expressible in
112
116
  // the basic catalog: it needs a custom catalog or renderer styling.
@@ -124,6 +128,14 @@ function cardComponents(heading: string): Component[] {
124
128
  { id: 'row_tpl', component: 'Row', children: ['row_label', 'row_value'] },
125
129
  { id: 'row_label', component: 'Text', text: '${label}', variant: 'caption' },
126
130
  { id: 'row_value', component: 'Text', text: '${value}', variant: 'caption' },
131
+
132
+ // The record's action: zero or one per card, so a card without one draws nothing. Pressing it
133
+ // dispatches nxis.ask with the message to send as the visitor; it never submits anything itself.
134
+ { id: 'card_actions', component: 'List',
135
+ children: { path: 'actions', componentId: 'action_tpl' }, direction: 'horizontal' },
136
+ { id: 'action_tpl', component: 'Button', child: 'action_label', variant: 'primary',
137
+ action: { event: { name: 'nxis.ask', context: { message: '${message}' } } } },
138
+ { id: 'action_label', component: 'Text', text: '${label}', variant: 'body' },
127
139
  ];
128
140
  }
129
141
 
@@ -0,0 +1,54 @@
1
+ /**
2
+ * The action a result card offers, chosen from the record's own `potentialAction`.
3
+ *
4
+ * One map, shared with the Typed Retrieval skill (nxis-skills) and the NxisAI app's widgets (nxis-mcp
5
+ * `HubWidgets`): the action's `@type` alone decides the button, and the button sends the same prompt
6
+ * everywhere ("Get a quote from Hatzel & Sons Plumbing"), so the funnel cannot tell which surface it came
7
+ * from. Commerce, read and search actions get no button here.
8
+ *
9
+ * On the site's own page every mapped action sends the prompt, a self-page form included: the on-page agent
10
+ * is what runs that form. Only the NxisAI app, which cannot reach the page, opens it instead.
11
+ *
12
+ * @module
13
+ */
14
+
15
+ /** What a card's action button says and sends. */
16
+ export interface CardAction {
17
+ label: string;
18
+ /** The message sent as the visitor when the button is pressed. */
19
+ message: string;
20
+ }
21
+
22
+ const MAP: Record<string, { label: string; message: (name: string) => string }> = {
23
+ QuoteAction: { label: 'Get a quote', message: (n) => `Get a quote from ${n}` },
24
+ AskAction: { label: 'Contact', message: (n) => `Contact ${n}` },
25
+ ReserveAction: { label: 'Reserve', message: (n) => `Reserve at ${n}` },
26
+ ScheduleAction: { label: 'Book an appointment', message: (n) => `Book an appointment at ${n}` },
27
+ ApplyAction: { label: 'Apply', message: (n) => `Apply to ${n}` },
28
+ };
29
+
30
+ /** When a record carries several, the first of these wins. */
31
+ const ORDER = Object.keys(MAP);
32
+
33
+ const shortType = (t: unknown): string =>
34
+ typeof t === 'string' ? t.replace(/^https?:\/\/schema\.org\//, '') : '';
35
+
36
+ const list = (v: unknown): unknown[] => (v == null ? [] : Array.isArray(v) ? v : [v]);
37
+
38
+ /**
39
+ * The button for a record, or none.
40
+ *
41
+ * @param record - The record as a tool returned it; action `@type`s may be full IRIs.
42
+ * @param name - What to call it in the message; the record's own name by default.
43
+ */
44
+ export function actionCta(record: Record<string, unknown>, name?: string): CardAction | undefined {
45
+ const types = list(record.potentialAction)
46
+ .filter((a): a is Record<string, unknown> => !!a && typeof a === 'object')
47
+ .flatMap((a) => list(a['@type']).map(shortType))
48
+ .filter((t) => t in MAP)
49
+ .sort((a, b) => ORDER.indexOf(a) - ORDER.indexOf(b));
50
+ if (!types.length) return undefined;
51
+ const who = (name ?? (typeof record.name === 'string' ? record.name : '')).trim() || 'them';
52
+ const entry = MAP[types[0]];
53
+ return { label: entry.label, message: entry.message(who) };
54
+ }
@@ -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
@@ -561,6 +575,48 @@ export class AgentHarness {
561
575
  return this.act(tool, call.args, messages);
562
576
  }
563
577
 
578
+ /**
579
+ * Run a turn whose tool call is already decided, without asking the model.
580
+ *
581
+ * For a request that names its own action, such as a link from the NxisAI
582
+ * app to one business's quote form: the tool and the business are known, so
583
+ * the model has nothing to choose, and a model left to choose sometimes
584
+ * asks for the details in prose instead of opening the form. What follows
585
+ * the call is an ordinary turn's: the same slots, prompts and confirmation,
586
+ * and the call goes into the transcript as if the model had made it.
587
+ *
588
+ * @param goal - The request as the person would have said it, for the thread and transcript.
589
+ * @param name - The tool to call.
590
+ * @param args - Its arguments, e.g. which business.
591
+ * @param messages - The live conversation, appended to in place.
592
+ * @returns The decision taken, or `'unavailable'` when this agent has no such tool.
593
+ */
594
+ async begin(goal: string, name: string, args: Record<string, unknown>, messages: ChatMessage[]): Promise<TurnResult> {
595
+ this.abort?.abort();
596
+ this.abort = new AbortController();
597
+ this.goal = goal;
598
+ this.renderer.stoppable?.(true);
599
+ this.applySystemPrompt(messages);
600
+ beginJourney();
601
+
602
+ const bubble = this.renderer.streaming();
603
+ bubble.thinking('Finding tools…');
604
+ const tools = this.visibleTools(await this.toolSource.discoverTools(this.abort.signal), goal);
605
+ if (this.stopped) return this.stopTurn(bubble, messages, goal);
606
+ const tool = tools.find((t) => t.name === name);
607
+ if (!tool) {
608
+ bubble.finalize('');
609
+ return this.finished({ decision: 'unavailable' as const });
610
+ }
611
+
612
+ const call = { name, args };
613
+ messages.push({ role: 'user', content: goal });
614
+ bubble.finalize(narrateCall(call, tools));
615
+ messages.push({ role: 'assistant', content: this.transcribe({ text: '', call } as Completion) });
616
+ this.opts.onMessages?.(messages);
617
+ return this.act(tool, args, messages);
618
+ }
619
+
564
620
  /**
565
621
  * Narrow the page's tools to this agent's slice.
566
622
  *
@@ -834,6 +890,35 @@ export class AgentHarness {
834
890
  this.renderer.step?.(step, journeySteps());
835
891
  }
836
892
 
893
+ /**
894
+ * The record to recommend from a search's results, when there are several
895
+ * to choose between and the model can say why from what they state.
896
+ */
897
+ private async pickFrom(res: ModelContextToolResponse): Promise<{ pick: ResultPick; name: string } | null> {
898
+ if (this.opts.recommend === false || !this.goal) return null;
899
+ // Records of things, not rows of a type listing: something with a name and a kind.
900
+ const records = extractRecords(res).filter((r) => typeof r.name === 'string' && r['@type'] != null);
901
+ if (records.length < 2) return null;
902
+ const bubble = this.renderer.streaming();
903
+ bubble.thinking('Comparing them…');
904
+ try {
905
+ const pick = await recommend({
906
+ provider: this.provider,
907
+ goal: this.goal,
908
+ records,
909
+ now: this.opts.now?.(),
910
+ signal: this.abort?.signal,
911
+ transcribe: withTranscribedCall,
912
+ });
913
+ return pick ? { pick, name: String(records[pick.index].name) } : null;
914
+ } catch {
915
+ // Stopped, or the model failed: the results stand on their own.
916
+ return null;
917
+ } finally {
918
+ bubble.finalize('');
919
+ }
920
+ }
921
+
837
922
  /** Discard a live prompt and the values typed into it. */
838
923
  private cancel(): void {
839
924
  // A journey that stops here should say so, not sit waiting for a press
@@ -974,8 +1059,14 @@ export class AgentHarness {
974
1059
  });
975
1060
 
976
1061
  if (readOnly) {
977
- this.renderer.results(tool, res);
978
- messages.push({ role: 'user', content: `[${tool.name} returned: ${text.slice(0, 400)}]` });
1062
+ const picked = await this.pickFrom(res);
1063
+ this.renderer.results(tool, res, picked ? { pick: picked.pick } : undefined);
1064
+ messages.push({
1065
+ role: 'user',
1066
+ content:
1067
+ `[${tool.name} returned: ${text.slice(0, 400)}]` +
1068
+ (picked ? ` [You recommended ${picked.name}: ${picked.pick.reasons.join(' ')}]` : ''),
1069
+ });
979
1070
  } else {
980
1071
  this.renderer.detail(tool, values, text);
981
1072
  messages.push({