@nxisai/agent-harness 0.8.0 → 0.10.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 (57) 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 +28 -2
  18. package/dist/harness/tools/action-tools.d.ts.map +1 -1
  19. package/dist/harness/tools/action-tools.js +48 -2
  20. package/dist/harness/tools/action-tools.js.map +1 -1
  21. package/dist/harness/tools/bridge.d.ts.map +1 -1
  22. package/dist/harness/tools/bridge.js +2 -1
  23. package/dist/harness/tools/bridge.js.map +1 -1
  24. package/dist/harness/tools/inbound.d.ts +92 -0
  25. package/dist/harness/tools/inbound.d.ts.map +1 -0
  26. package/dist/harness/tools/inbound.js +170 -0
  27. package/dist/harness/tools/inbound.js.map +1 -0
  28. package/dist/harness/tools/report.d.ts +55 -0
  29. package/dist/harness/tools/report.d.ts.map +1 -0
  30. package/dist/harness/tools/report.js +75 -0
  31. package/dist/harness/tools/report.js.map +1 -0
  32. package/dist/harness/tools/source.d.ts.map +1 -1
  33. package/dist/harness/tools/source.js +17 -1
  34. package/dist/harness/tools/source.js.map +1 -1
  35. package/dist/harness/tools/webmcp.d.ts.map +1 -1
  36. package/dist/harness/tools/webmcp.js +5 -1
  37. package/dist/harness/tools/webmcp.js.map +1 -1
  38. package/dist/index.d.ts +5 -0
  39. package/dist/index.d.ts.map +1 -1
  40. package/dist/index.js +4 -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 +78 -12
  44. package/dist/ui/agent-fullscreen.js.map +1 -1
  45. package/package.json +1 -1
  46. package/src/a2ui/cards.ts +14 -2
  47. package/src/harness/cta.ts +54 -0
  48. package/src/harness/harness.ts +42 -0
  49. package/src/harness/router/coordinator.ts +14 -0
  50. package/src/harness/tools/action-tools.ts +51 -2
  51. package/src/harness/tools/bridge.ts +2 -1
  52. package/src/harness/tools/inbound.ts +204 -0
  53. package/src/harness/tools/report.ts +119 -0
  54. package/src/harness/tools/source.ts +18 -1
  55. package/src/harness/tools/webmcp.ts +5 -1
  56. package/src/index.ts +14 -0
  57. package/src/ui/agent-fullscreen.ts +72 -5
package/src/index.ts CHANGED
@@ -50,6 +50,9 @@ export type { Slot, Resolution, ResolveSources } from './harness/slots.js';
50
50
 
51
51
  export { buildPlan } from './harness/plan.js';
52
52
 
53
+ export { actionCta } from './harness/cta.js';
54
+ export type { CardAction } from './harness/cta.js';
55
+
53
56
  export { AgentCoordinator } from './harness/router/coordinator.js';
54
57
  export type { CoordinatorOptions, RoutedTurn, RouteSource } from './harness/router/coordinator.js';
55
58
  export { matchRoute } from './harness/router/rules.js';
@@ -134,6 +137,16 @@ export {
134
137
  export type { AgentStep, AgentStepId, AgentStepStatus } from './harness/steps.js';
135
138
  export { resumePendingAction } from './harness/tools/resume.js';
136
139
  export type { ResumeOutcome, ResumeOptions } from './harness/tools/resume.js';
140
+ // Arriving from the NxisAI app to use one of this page's forms.
141
+ export {
142
+ HANDOFF_PARAM,
143
+ INTENT_PARAM,
144
+ readInboundHandoff,
145
+ takeInboundHandoff,
146
+ handoffUrl,
147
+ findHandoffForm,
148
+ } from './harness/tools/inbound.js';
149
+ export type { InboundHandoff, ArrivalOutcome, ArrivalOptions } from './harness/tools/inbound.js';
137
150
  export {
138
151
  loadSitePages,
139
152
  parsePage,
@@ -145,6 +158,7 @@ export {
145
158
  export type { SitePage, SitePagesOptions } from './harness/tools/site-pages.js';
146
159
  export { buildOpenPageTool, registerOpenPageTool, resolveDestination, visitFor, OPEN_PAGE_TOOL } from './harness/tools/open-page.js';
147
160
  export { isRateLimited, retryAfterMs, withRetry } from './harness/tools/retry.js';
161
+ export { setSiteActionReporter, TRACK_TOOL, type PageFormRun, type SiteActionReport, type SiteActionReporter } from './harness/tools/report.js';
148
162
  export type { RetryOptions } from './harness/tools/retry.js';
149
163
  export {
150
164
  applyTrails,
@@ -22,6 +22,7 @@
22
22
  */
23
23
  import { mountSurface, type MountedSurface } from '../a2ui/host.js';
24
24
  import { emitCardsSurface, type ResultCardData } from '../a2ui/cards.js';
25
+ import { actionCta, type CardAction } 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
@@ -46,6 +47,7 @@ import {
46
47
  formsFor,
47
48
  handoffFor,
48
49
  isSiteTool,
50
+ siteFormFor,
49
51
  withoutSiteSuffix,
50
52
  HANDOFF_RESULT,
51
53
  } from '../harness/tools/action-tools.js';
@@ -64,6 +66,7 @@ import { resultsAnswer, RESULTS_ANSWER_CSS, type AnswerCard } from './result-pic
64
66
  import { extractRecords, openStatus, recordFacts } from '../harness/records.js';
65
67
  import type { ResultPick } from '../harness/recommend.js';
66
68
  import { resumePendingAction } from '../harness/tools/resume.js';
69
+ import { findHandoffForm, startFormMessage, startMessage, takeInboundHandoff } from '../harness/tools/inbound.js';
67
70
  import { savePendingVisit, setConversationSource, takePendingVisit } from '../harness/tools/pending-action.js';
68
71
  import { pageForEntity, type SitePage } from '../harness/tools/site-pages.js';
69
72
  import { OPEN_PAGE_TOOL } from '../harness/tools/open-page.js';
@@ -341,6 +344,8 @@ interface ResultCard {
341
344
  phone?: string;
342
345
  /** What the record states, short, for comparing it. */
343
346
  facts?: string[];
347
+ /** The action the record itself offers (its potentialAction), as a button. */
348
+ action?: CardAction;
344
349
  }
345
350
 
346
351
  type ThreadItem =
@@ -682,6 +687,8 @@ class NxisAgentFullscreen extends HTMLElement {
682
687
  // belongs to is the one open.
683
688
  void this._resumePendingAction();
684
689
  this._resumePendingVisit();
690
+ // Sent here from the NxisAI app to use one of this page's forms.
691
+ void this._arriveFromHandoff();
685
692
 
686
693
  this._els.launcher.addEventListener('click', () => this._open());
687
694
  this._els.handoffBack.addEventListener('click', () => void this._endHandoff());
@@ -1589,7 +1596,14 @@ class NxisAgentFullscreen extends HTMLElement {
1589
1596
  }
1590
1597
 
1591
1598
  // ── Turn loop ──────────────────────────────────────────────────────────────
1592
- private async _onSend(): Promise<void> {
1599
+ /**
1600
+ * Send what is in the composer.
1601
+ *
1602
+ * @param call - A tool call already decided, run without asking the model
1603
+ * (a handoff names its form). Falls back to an ordinary turn when no agent
1604
+ * here holds the tool.
1605
+ */
1606
+ private async _onSend(call?: { name: string; args: Record<string, unknown> }): Promise<void> {
1593
1607
  const goal = this._els.input.value.trim();
1594
1608
  if (!goal || this._turnActive) return;
1595
1609
  this._els.input.value = '';
@@ -1626,7 +1640,9 @@ class NxisAgentFullscreen extends HTMLElement {
1626
1640
  await this._connectTools();
1627
1641
  this._syncProfileContext();
1628
1642
  // One agent, or several with a router in front: the signatures match.
1629
- await (this._coordinator ?? this._harness).turn(goal, conv!.messages);
1643
+ const engine = this._coordinator ?? this._harness;
1644
+ const begun = call ? await engine.begin(goal, call.name, call.args, conv!.messages) : null;
1645
+ if (!begun || begun.decision === 'unavailable') await engine.turn(goal, conv!.messages);
1630
1646
  // A remote endpoint reports which model answered; the name beside the
1631
1647
  // input follows it rather than keeping whatever was configured.
1632
1648
  this._paintProvenance();
@@ -2673,6 +2689,7 @@ class NxisAgentFullscreen extends HTMLElement {
2673
2689
  facts.since ? `In business since ${facts.since}` : '',
2674
2690
  facts.reviews[0] ? `\u201c${facts.reviews[0].text}\u201d${facts.reviews[0].author ? ` \u2014 ${facts.reviews[0].author}` : ''}` : '',
2675
2691
  ].filter((l): l is string => !!l);
2692
+ const action = actionCta(entity, title);
2676
2693
  return {
2677
2694
  title,
2678
2695
  type,
@@ -2686,6 +2703,7 @@ class NxisAgentFullscreen extends HTMLElement {
2686
2703
  ...(facts.reviewCount !== undefined ? { reviews: facts.reviewCount } : {}),
2687
2704
  ...(status ? { status: { open: status.open, label: status.label } } : {}),
2688
2705
  ...(facts.phone ? { phone: facts.phone } : {}),
2706
+ ...(action ? { action } : {}),
2689
2707
  facts: lines,
2690
2708
  };
2691
2709
  }
@@ -2714,7 +2732,9 @@ class NxisAgentFullscreen extends HTMLElement {
2714
2732
  const things = item.cards.filter((c) => c.stars !== undefined || c.status || c.phone || c.page);
2715
2733
  if (!item.pick && !things.length) return null;
2716
2734
  const cards: AnswerCard[] = item.cards.map((c) => {
2717
- const form = formsFor(c.title)[0];
2735
+ // The record's own action first: the same map and prompt as the Typed Retrieval skill and the NxisAI
2736
+ // app. A record that states none falls back to a form found on this page for it.
2737
+ const form = c.action ? undefined : formsFor(c.title)[0];
2718
2738
  const quote = form && /quote/.test(form.noun);
2719
2739
  return {
2720
2740
  title: c.title,
@@ -2724,7 +2744,9 @@ class NxisAgentFullscreen extends HTMLElement {
2724
2744
  phone: c.phone,
2725
2745
  page: c.page,
2726
2746
  facts: c.facts ?? [],
2727
- ...(form
2747
+ ...(c.action
2748
+ ? { form: { label: c.action.label, message: c.action.message } }
2749
+ : form
2728
2750
  ? {
2729
2751
  form: quote
2730
2752
  ? { label: 'Get a quote', message: `Get a quote from ${c.title}` }
@@ -2762,6 +2784,7 @@ class NxisAgentFullscreen extends HTMLElement {
2762
2784
  ...(c.emergency ? [{ label: '24/7' }] : []),
2763
2785
  ],
2764
2786
  rows: c.rows.map((r) => ({ label: r.label, value: r.value })),
2787
+ actions: c.action ? [c.action] : [],
2765
2788
  }));
2766
2789
  }
2767
2790
 
@@ -2775,7 +2798,15 @@ class NxisAgentFullscreen extends HTMLElement {
2775
2798
  }
2776
2799
  try {
2777
2800
  const messages = emitCardsSurface(item.note, this._toA2uiCards(item.cards));
2778
- const mounted = mountSurface(wrap, messages);
2801
+ const mounted = mountSurface(wrap, messages, (event: any) => {
2802
+ const name = event?.event?.name ?? event?.name ?? '';
2803
+ const ctx = (event?.event?.context ?? event?.context ?? {}) as Record<string, unknown>;
2804
+ // A card's action button: send its message as the visitor, as the inline card does.
2805
+ if (name === 'nxis.ask' && typeof ctx.message === 'string' && !this._turnActive) {
2806
+ this._els.input.value = ctx.message;
2807
+ void this._onSend();
2808
+ }
2809
+ });
2779
2810
  if (!mounted) throw new Error('no surface produced');
2780
2811
  this._surfaces.push(mounted);
2781
2812
  } catch (err) {
@@ -3503,6 +3534,42 @@ class NxisAgentFullscreen extends HTMLElement {
3503
3534
  if (this.mode !== 'fullscreen') this._open();
3504
3535
  }
3505
3536
 
3537
+ /**
3538
+ * Open on the form a link from the NxisAI app names.
3539
+ *
3540
+ * In a new conversation: the person arrives from another app, and nothing
3541
+ * on this site led here. The agent starts the form in its own words, so the
3542
+ * link only chooses which form; what the person wanted waits in the
3543
+ * composer for them to send, change or delete.
3544
+ */
3545
+ private async _arriveFromHandoff(): Promise<void> {
3546
+ const handoff = takeInboundHandoff();
3547
+ if (!handoff) return;
3548
+ this._newConversation();
3549
+ this._open();
3550
+ this._setStatus('Opening the form…');
3551
+ // The site's own tools first: they reach a form from any page, from the
3552
+ // site's records, so they work where the page registers nothing itself.
3553
+ await this._connectTools();
3554
+ const site = siteFormFor(handoff.actionId);
3555
+ const start = site
3556
+ ? startFormMessage(site.noun, site.choice)
3557
+ : await findHandoffForm(handoff).then((o) => (o.status === 'ready' ? startMessage(o.tool) : null));
3558
+ this._setStatus('');
3559
+ if (!start) {
3560
+ this._appendMsg('assistant', "I couldn't find that form on this page. Tell me what you need and I'll help from here.");
3561
+ } else if (!this._turnActive) {
3562
+ this._els.input.value = start;
3563
+ // The form is known, so it opens without the model choosing it: left to
3564
+ // choose, a model sometimes asks for the details in prose instead.
3565
+ await this._onSend(site ? { name: site.tool, args: site.args } : undefined);
3566
+ }
3567
+ if (handoff.intent && !this._els.input.value) {
3568
+ this._els.input.value = handoff.intent;
3569
+ this._autosize();
3570
+ }
3571
+ }
3572
+
3506
3573
  private async _resumePendingAction(): Promise<void> {
3507
3574
  const outcome = await resumePendingAction({
3508
3575
  onStart: (pending) => {