@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.
Files changed (40) hide show
  1. package/dist/a2ui/cards.d.ts +3 -0
  2. package/dist/a2ui/cards.d.ts.map +1 -1
  3. package/dist/a2ui/cards.js +11 -2
  4. package/dist/a2ui/cards.js.map +1 -1
  5. package/dist/harness/cta.d.ts +27 -0
  6. package/dist/harness/cta.d.ts.map +1 -0
  7. package/dist/harness/cta.js +43 -0
  8. package/dist/harness/cta.js.map +1 -0
  9. package/dist/harness/harness.d.ts +17 -0
  10. package/dist/harness/harness.d.ts.map +1 -1
  11. package/dist/harness/harness.js +40 -0
  12. package/dist/harness/harness.js.map +1 -1
  13. package/dist/harness/router/coordinator.d.ts +6 -0
  14. package/dist/harness/router/coordinator.d.ts.map +1 -1
  15. package/dist/harness/router/coordinator.js +14 -0
  16. package/dist/harness/router/coordinator.js.map +1 -1
  17. package/dist/harness/tools/action-tools.d.ts +15 -0
  18. package/dist/harness/tools/action-tools.d.ts.map +1 -1
  19. package/dist/harness/tools/action-tools.js +21 -0
  20. package/dist/harness/tools/action-tools.js.map +1 -1
  21. package/dist/harness/tools/inbound.d.ts +88 -0
  22. package/dist/harness/tools/inbound.d.ts.map +1 -0
  23. package/dist/harness/tools/inbound.js +156 -0
  24. package/dist/harness/tools/inbound.js.map +1 -0
  25. package/dist/index.d.ts +4 -0
  26. package/dist/index.d.ts.map +1 -1
  27. package/dist/index.js +3 -0
  28. package/dist/index.js.map +1 -1
  29. package/dist/ui/agent-fullscreen.d.ts.map +1 -1
  30. package/dist/ui/agent-fullscreen.js +78 -12
  31. package/dist/ui/agent-fullscreen.js.map +1 -1
  32. package/package.json +1 -1
  33. package/src/a2ui/cards.ts +14 -2
  34. package/src/harness/cta.ts +54 -0
  35. package/src/harness/harness.ts +42 -0
  36. package/src/harness/router/coordinator.ts +14 -0
  37. package/src/harness/tools/action-tools.ts +23 -0
  38. package/src/harness/tools/inbound.ts +189 -0
  39. package/src/index.ts +13 -0
  40. 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
- async _onSend() {
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
- await (this._coordinator ?? this._harness).turn(goal, conv.messages);
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
- const form = formsFor(c.title)[0];
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
- ...(form
2376
- ? {
2377
- form: quote
2378
- ? { label: 'Get a quote', message: `Get a quote from ${c.title}` }
2379
- : { label: `Use their ${form.noun}`, message: `Start the ${form.noun} for ${c.title}` },
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) => {