@nxisai/agent-harness 0.8.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.
- package/dist/a2ui/cards.d.ts +3 -0
- package/dist/a2ui/cards.d.ts.map +1 -1
- package/dist/a2ui/cards.js +11 -2
- package/dist/a2ui/cards.js.map +1 -1
- package/dist/harness/cta.d.ts +27 -0
- package/dist/harness/cta.d.ts.map +1 -0
- package/dist/harness/cta.js +43 -0
- package/dist/harness/cta.js.map +1 -0
- package/dist/harness/harness.d.ts +17 -0
- package/dist/harness/harness.d.ts.map +1 -1
- package/dist/harness/harness.js +40 -0
- package/dist/harness/harness.js.map +1 -1
- package/dist/harness/router/coordinator.d.ts +6 -0
- package/dist/harness/router/coordinator.d.ts.map +1 -1
- package/dist/harness/router/coordinator.js +14 -0
- package/dist/harness/router/coordinator.js.map +1 -1
- package/dist/harness/tools/action-tools.d.ts +15 -0
- package/dist/harness/tools/action-tools.d.ts.map +1 -1
- package/dist/harness/tools/action-tools.js +21 -0
- package/dist/harness/tools/action-tools.js.map +1 -1
- package/dist/harness/tools/inbound.d.ts +88 -0
- package/dist/harness/tools/inbound.d.ts.map +1 -0
- package/dist/harness/tools/inbound.js +156 -0
- package/dist/harness/tools/inbound.js.map +1 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -0
- package/dist/index.js.map +1 -1
- package/dist/ui/agent-fullscreen.d.ts.map +1 -1
- package/dist/ui/agent-fullscreen.js +78 -12
- package/dist/ui/agent-fullscreen.js.map +1 -1
- package/package.json +1 -1
- package/src/a2ui/cards.ts +14 -2
- package/src/harness/cta.ts +54 -0
- package/src/harness/harness.ts +42 -0
- package/src/harness/router/coordinator.ts +14 -0
- package/src/harness/tools/action-tools.ts +23 -0
- package/src/harness/tools/inbound.ts +189 -0
- package/src/index.ts +13 -0
- package/src/ui/agent-fullscreen.ts +72 -5
|
@@ -22,6 +22,7 @@
|
|
|
22
22
|
*/
|
|
23
23
|
import { mountSurface } from '../a2ui/host.js';
|
|
24
24
|
import { emitCardsSurface } from '../a2ui/cards.js';
|
|
25
|
+
import { actionCta } from '../harness/cta.js';
|
|
25
26
|
import { emitToolForm } from '../a2ui/form.js';
|
|
26
27
|
import { A2UI_THEME_CSS } from '../a2ui/theme.js';
|
|
27
28
|
// Compiled from agent.tw.css at build time (scripts/css-to-ts.mjs). It is a
|
|
@@ -34,13 +35,14 @@ import { AgentHarness, isAbort } from '../harness/harness.js';
|
|
|
34
35
|
import { WebMcpTools } from '../harness/tools/webmcp.js';
|
|
35
36
|
import { extractResult, proseAnswer, missingRequired, toolSchema, } from '../harness/tools/schema.js';
|
|
36
37
|
import { connectTools } from '../harness/tools/source.js';
|
|
37
|
-
import { chooserOf, formOptions, formsFor, handoffFor, isSiteTool, withoutSiteSuffix, HANDOFF_RESULT, } from '../harness/tools/action-tools.js';
|
|
38
|
+
import { chooserOf, formOptions, formsFor, handoffFor, isSiteTool, siteFormFor, withoutSiteSuffix, HANDOFF_RESULT, } from '../harness/tools/action-tools.js';
|
|
38
39
|
import { resolveSlots } from '../harness/slots.js';
|
|
39
40
|
import { isHandoffTool } from '../handoff/tools.js';
|
|
40
41
|
import { actionCard, fieldLabel, fieldsFromSlots, ACTION_CARD_CSS, } from './action-card.js';
|
|
41
42
|
import { resultsAnswer, RESULTS_ANSWER_CSS } from './result-pick.js';
|
|
42
43
|
import { extractRecords, openStatus, recordFacts } from '../harness/records.js';
|
|
43
44
|
import { resumePendingAction } from '../harness/tools/resume.js';
|
|
45
|
+
import { findHandoffForm, startFormMessage, startMessage, takeInboundHandoff } from '../harness/tools/inbound.js';
|
|
44
46
|
import { savePendingVisit, setConversationSource, takePendingVisit } from '../harness/tools/pending-action.js';
|
|
45
47
|
import { pageForEntity } from '../harness/tools/site-pages.js';
|
|
46
48
|
import { OPEN_PAGE_TOOL } from '../harness/tools/open-page.js';
|
|
@@ -415,6 +417,8 @@ class NxisAgentFullscreen extends HTMLElement {
|
|
|
415
417
|
// belongs to is the one open.
|
|
416
418
|
void this._resumePendingAction();
|
|
417
419
|
this._resumePendingVisit();
|
|
420
|
+
// Sent here from the NxisAI app to use one of this page's forms.
|
|
421
|
+
void this._arriveFromHandoff();
|
|
418
422
|
this._els.launcher.addEventListener('click', () => this._open());
|
|
419
423
|
this._els.handoffBack.addEventListener('click', () => void this._endHandoff());
|
|
420
424
|
// A handoff left open on the last page carries on here.
|
|
@@ -1304,7 +1308,14 @@ class NxisAgentFullscreen extends HTMLElement {
|
|
|
1304
1308
|
return bubble;
|
|
1305
1309
|
}
|
|
1306
1310
|
// ── Turn loop ──────────────────────────────────────────────────────────────
|
|
1307
|
-
|
|
1311
|
+
/**
|
|
1312
|
+
* Send what is in the composer.
|
|
1313
|
+
*
|
|
1314
|
+
* @param call - A tool call already decided, run without asking the model
|
|
1315
|
+
* (a handoff names its form). Falls back to an ordinary turn when no agent
|
|
1316
|
+
* here holds the tool.
|
|
1317
|
+
*/
|
|
1318
|
+
async _onSend(call) {
|
|
1308
1319
|
const goal = this._els.input.value.trim();
|
|
1309
1320
|
if (!goal || this._turnActive)
|
|
1310
1321
|
return;
|
|
@@ -1340,7 +1351,10 @@ class NxisAgentFullscreen extends HTMLElement {
|
|
|
1340
1351
|
await this._connectTools();
|
|
1341
1352
|
this._syncProfileContext();
|
|
1342
1353
|
// One agent, or several with a router in front: the signatures match.
|
|
1343
|
-
|
|
1354
|
+
const engine = this._coordinator ?? this._harness;
|
|
1355
|
+
const begun = call ? await engine.begin(goal, call.name, call.args, conv.messages) : null;
|
|
1356
|
+
if (!begun || begun.decision === 'unavailable')
|
|
1357
|
+
await engine.turn(goal, conv.messages);
|
|
1344
1358
|
// A remote endpoint reports which model answered; the name beside the
|
|
1345
1359
|
// input follows it rather than keeping whatever was configured.
|
|
1346
1360
|
this._paintProvenance();
|
|
@@ -2326,6 +2340,7 @@ class NxisAgentFullscreen extends HTMLElement {
|
|
|
2326
2340
|
facts.since ? `In business since ${facts.since}` : '',
|
|
2327
2341
|
facts.reviews[0] ? `\u201c${facts.reviews[0].text}\u201d${facts.reviews[0].author ? ` \u2014 ${facts.reviews[0].author}` : ''}` : '',
|
|
2328
2342
|
].filter((l) => !!l);
|
|
2343
|
+
const action = actionCta(entity, title);
|
|
2329
2344
|
return {
|
|
2330
2345
|
title,
|
|
2331
2346
|
type,
|
|
@@ -2339,6 +2354,7 @@ class NxisAgentFullscreen extends HTMLElement {
|
|
|
2339
2354
|
...(facts.reviewCount !== undefined ? { reviews: facts.reviewCount } : {}),
|
|
2340
2355
|
...(status ? { status: { open: status.open, label: status.label } } : {}),
|
|
2341
2356
|
...(facts.phone ? { phone: facts.phone } : {}),
|
|
2357
|
+
...(action ? { action } : {}),
|
|
2342
2358
|
facts: lines,
|
|
2343
2359
|
};
|
|
2344
2360
|
}
|
|
@@ -2362,7 +2378,9 @@ class NxisAgentFullscreen extends HTMLElement {
|
|
|
2362
2378
|
if (!item.pick && !things.length)
|
|
2363
2379
|
return null;
|
|
2364
2380
|
const cards = item.cards.map((c) => {
|
|
2365
|
-
|
|
2381
|
+
// The record's own action first: the same map and prompt as the Typed Retrieval skill and the NxisAI
|
|
2382
|
+
// app. A record that states none falls back to a form found on this page for it.
|
|
2383
|
+
const form = c.action ? undefined : formsFor(c.title)[0];
|
|
2366
2384
|
const quote = form && /quote/.test(form.noun);
|
|
2367
2385
|
return {
|
|
2368
2386
|
title: c.title,
|
|
@@ -2372,13 +2390,15 @@ class NxisAgentFullscreen extends HTMLElement {
|
|
|
2372
2390
|
phone: c.phone,
|
|
2373
2391
|
page: c.page,
|
|
2374
2392
|
facts: c.facts ?? [],
|
|
2375
|
-
...(
|
|
2376
|
-
? {
|
|
2377
|
-
|
|
2378
|
-
|
|
2379
|
-
|
|
2380
|
-
|
|
2381
|
-
|
|
2393
|
+
...(c.action
|
|
2394
|
+
? { form: { label: c.action.label, message: c.action.message } }
|
|
2395
|
+
: form
|
|
2396
|
+
? {
|
|
2397
|
+
form: quote
|
|
2398
|
+
? { label: 'Get a quote', message: `Get a quote from ${c.title}` }
|
|
2399
|
+
: { label: `Use their ${form.noun}`, message: `Start the ${form.noun} for ${c.title}` },
|
|
2400
|
+
}
|
|
2401
|
+
: {}),
|
|
2382
2402
|
};
|
|
2383
2403
|
});
|
|
2384
2404
|
return resultsAnswer(cards, item.pick, {
|
|
@@ -2410,6 +2430,7 @@ class NxisAgentFullscreen extends HTMLElement {
|
|
|
2410
2430
|
...(c.emergency ? [{ label: '24/7' }] : []),
|
|
2411
2431
|
],
|
|
2412
2432
|
rows: c.rows.map((r) => ({ label: r.label, value: r.value })),
|
|
2433
|
+
actions: c.action ? [c.action] : [],
|
|
2413
2434
|
}));
|
|
2414
2435
|
}
|
|
2415
2436
|
/** Results rendered through A2UI instead of the bespoke card markup. */
|
|
@@ -2422,7 +2443,15 @@ class NxisAgentFullscreen extends HTMLElement {
|
|
|
2422
2443
|
}
|
|
2423
2444
|
try {
|
|
2424
2445
|
const messages = emitCardsSurface(item.note, this._toA2uiCards(item.cards));
|
|
2425
|
-
const mounted = mountSurface(wrap, messages)
|
|
2446
|
+
const mounted = mountSurface(wrap, messages, (event) => {
|
|
2447
|
+
const name = event?.event?.name ?? event?.name ?? '';
|
|
2448
|
+
const ctx = (event?.event?.context ?? event?.context ?? {});
|
|
2449
|
+
// A card's action button: send its message as the visitor, as the inline card does.
|
|
2450
|
+
if (name === 'nxis.ask' && typeof ctx.message === 'string' && !this._turnActive) {
|
|
2451
|
+
this._els.input.value = ctx.message;
|
|
2452
|
+
void this._onSend();
|
|
2453
|
+
}
|
|
2454
|
+
});
|
|
2426
2455
|
if (!mounted)
|
|
2427
2456
|
throw new Error('no surface produced');
|
|
2428
2457
|
this._surfaces.push(mounted);
|
|
@@ -3127,6 +3156,43 @@ class NxisAgentFullscreen extends HTMLElement {
|
|
|
3127
3156
|
if (this.mode !== 'fullscreen')
|
|
3128
3157
|
this._open();
|
|
3129
3158
|
}
|
|
3159
|
+
/**
|
|
3160
|
+
* Open on the form a link from the NxisAI app names.
|
|
3161
|
+
*
|
|
3162
|
+
* In a new conversation: the person arrives from another app, and nothing
|
|
3163
|
+
* on this site led here. The agent starts the form in its own words, so the
|
|
3164
|
+
* link only chooses which form; what the person wanted waits in the
|
|
3165
|
+
* composer for them to send, change or delete.
|
|
3166
|
+
*/
|
|
3167
|
+
async _arriveFromHandoff() {
|
|
3168
|
+
const handoff = takeInboundHandoff();
|
|
3169
|
+
if (!handoff)
|
|
3170
|
+
return;
|
|
3171
|
+
this._newConversation();
|
|
3172
|
+
this._open();
|
|
3173
|
+
this._setStatus('Opening the form…');
|
|
3174
|
+
// The site's own tools first: they reach a form from any page, from the
|
|
3175
|
+
// site's records, so they work where the page registers nothing itself.
|
|
3176
|
+
await this._connectTools();
|
|
3177
|
+
const site = siteFormFor(handoff.actionId);
|
|
3178
|
+
const start = site
|
|
3179
|
+
? startFormMessage(site.noun, site.choice)
|
|
3180
|
+
: await findHandoffForm(handoff).then((o) => (o.status === 'ready' ? startMessage(o.tool) : null));
|
|
3181
|
+
this._setStatus('');
|
|
3182
|
+
if (!start) {
|
|
3183
|
+
this._appendMsg('assistant', "I couldn't find that form on this page. Tell me what you need and I'll help from here.");
|
|
3184
|
+
}
|
|
3185
|
+
else if (!this._turnActive) {
|
|
3186
|
+
this._els.input.value = start;
|
|
3187
|
+
// The form is known, so it opens without the model choosing it: left to
|
|
3188
|
+
// choose, a model sometimes asks for the details in prose instead.
|
|
3189
|
+
await this._onSend(site ? { name: site.tool, args: site.args } : undefined);
|
|
3190
|
+
}
|
|
3191
|
+
if (handoff.intent && !this._els.input.value) {
|
|
3192
|
+
this._els.input.value = handoff.intent;
|
|
3193
|
+
this._autosize();
|
|
3194
|
+
}
|
|
3195
|
+
}
|
|
3130
3196
|
async _resumePendingAction() {
|
|
3131
3197
|
const outcome = await resumePendingAction({
|
|
3132
3198
|
onStart: (pending) => {
|