@nxisai/agent-harness 0.4.0 → 0.5.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 (103) hide show
  1. package/README.md +4 -2
  2. package/dist/harness/harness.d.ts.map +1 -1
  3. package/dist/harness/harness.js +31 -9
  4. package/dist/harness/harness.js.map +1 -1
  5. package/dist/harness/tools/action-tools.d.ts.map +1 -1
  6. package/dist/harness/tools/action-tools.js +7 -5
  7. package/dist/harness/tools/action-tools.js.map +1 -1
  8. package/dist/harness/tools/bridge.d.ts.map +1 -1
  9. package/dist/harness/tools/bridge.js +33 -12
  10. package/dist/harness/tools/bridge.js.map +1 -1
  11. package/dist/harness/tools/local-context.d.ts +49 -9
  12. package/dist/harness/tools/local-context.d.ts.map +1 -1
  13. package/dist/harness/tools/local-context.js +377 -22
  14. package/dist/harness/tools/local-context.js.map +1 -1
  15. package/dist/harness/tools/open-page.d.ts +47 -1
  16. package/dist/harness/tools/open-page.d.ts.map +1 -1
  17. package/dist/harness/tools/open-page.js +119 -33
  18. package/dist/harness/tools/open-page.js.map +1 -1
  19. package/dist/harness/tools/retry.d.ts +53 -0
  20. package/dist/harness/tools/retry.d.ts.map +1 -0
  21. package/dist/harness/tools/retry.js +96 -0
  22. package/dist/harness/tools/retry.js.map +1 -0
  23. package/dist/harness/tools/schema.d.ts +18 -0
  24. package/dist/harness/tools/schema.d.ts.map +1 -1
  25. package/dist/harness/tools/schema.js +38 -0
  26. package/dist/harness/tools/schema.js.map +1 -1
  27. package/dist/harness/tools/site-actions.d.ts.map +1 -1
  28. package/dist/harness/tools/site-actions.js +3 -2
  29. package/dist/harness/tools/site-actions.js.map +1 -1
  30. package/dist/harness/tools/site-nav.d.ts +168 -0
  31. package/dist/harness/tools/site-nav.d.ts.map +1 -0
  32. package/dist/harness/tools/site-nav.js +316 -0
  33. package/dist/harness/tools/site-nav.js.map +1 -0
  34. package/dist/harness/tools/site-pages.d.ts +9 -4
  35. package/dist/harness/tools/site-pages.d.ts.map +1 -1
  36. package/dist/harness/tools/site-pages.js +46 -7
  37. package/dist/harness/tools/site-pages.js.map +1 -1
  38. package/dist/harness/tools/source.d.ts +11 -2
  39. package/dist/harness/tools/source.d.ts.map +1 -1
  40. package/dist/harness/tools/source.js +55 -15
  41. package/dist/harness/tools/source.js.map +1 -1
  42. package/dist/harness/tools/webmcp.d.ts +39 -3
  43. package/dist/harness/tools/webmcp.d.ts.map +1 -1
  44. package/dist/harness/tools/webmcp.js +93 -14
  45. package/dist/harness/tools/webmcp.js.map +1 -1
  46. package/dist/index.d.ts +7 -3
  47. package/dist/index.d.ts.map +1 -1
  48. package/dist/index.js +4 -2
  49. package/dist/index.js.map +1 -1
  50. package/dist/types.d.ts +2 -0
  51. package/dist/types.d.ts.map +1 -1
  52. package/dist/ui/agent-fullscreen.d.ts.map +1 -1
  53. package/dist/ui/agent-fullscreen.js +13 -2
  54. package/dist/ui/agent-fullscreen.js.map +1 -1
  55. package/dist/ui/tailwind.generated.d.ts +1 -1
  56. package/dist/ui/tailwind.generated.d.ts.map +1 -1
  57. package/dist/ui/tailwind.generated.js +1 -1
  58. package/dist/ui/tailwind.generated.js.map +1 -1
  59. package/package.json +2 -1
  60. package/src/a2ui/cards.ts +177 -0
  61. package/src/a2ui/emit.ts +147 -0
  62. package/src/a2ui/form.ts +117 -0
  63. package/src/a2ui/host.ts +73 -0
  64. package/src/a2ui/index.ts +22 -0
  65. package/src/a2ui/stepper.ts +211 -0
  66. package/src/a2ui/theme.ts +134 -0
  67. package/src/a2ui/to-v09.ts +118 -0
  68. package/src/engine.ts +701 -0
  69. package/src/harness/harness.ts +956 -0
  70. package/src/harness/plan.ts +91 -0
  71. package/src/harness/policy.ts +59 -0
  72. package/src/harness/renderer.ts +208 -0
  73. package/src/harness/router/coordinator.ts +220 -0
  74. package/src/harness/router/rules.ts +65 -0
  75. package/src/harness/slots.ts +117 -0
  76. package/src/harness/steps.ts +156 -0
  77. package/src/harness/tools/action-tools.ts +870 -0
  78. package/src/harness/tools/bridge.ts +393 -0
  79. package/src/harness/tools/client.ts +26 -0
  80. package/src/harness/tools/discovery.ts +55 -0
  81. package/src/harness/tools/local-context.ts +480 -0
  82. package/src/harness/tools/open-page.ts +273 -0
  83. package/src/harness/tools/pending-action.ts +232 -0
  84. package/src/harness/tools/resume.ts +134 -0
  85. package/src/harness/tools/retry.ts +117 -0
  86. package/src/harness/tools/schema.ts +245 -0
  87. package/src/harness/tools/site-actions.ts +356 -0
  88. package/src/harness/tools/site-nav.ts +432 -0
  89. package/src/harness/tools/site-pages.ts +288 -0
  90. package/src/harness/tools/source.ts +353 -0
  91. package/src/harness/tools/webmcp.ts +200 -0
  92. package/src/index.ts +193 -0
  93. package/src/providers/capability.ts +167 -0
  94. package/src/providers/proxy.ts +191 -0
  95. package/src/providers/types.ts +150 -0
  96. package/src/types.ts +258 -0
  97. package/src/ui/agent-fullscreen.ts +3984 -0
  98. package/src/ui/agent.tw.css +49 -0
  99. package/src/ui/config.ts +138 -0
  100. package/src/ui/index.ts +32 -0
  101. package/src/ui/journey.ts +57 -0
  102. package/src/ui/tailwind.generated.ts +2 -0
  103. package/src/webmcp-contract.ts +65 -0
@@ -0,0 +1,168 @@
1
+ /**
2
+ * The site as a graph the agent can walk.
3
+ *
4
+ * A page publishes one breadcrumb trail: the path from the site's root down to
5
+ * itself. Every page publishes its own, so the trails together *are* the
6
+ * hierarchy — inverted, each page gains the sections above it, the pages beside
7
+ * it, and the pages under it. A page's `significantLink` and `relatedLink` add
8
+ * the cross-links a hierarchy cannot express: a listing pointing at its service
9
+ * area, a guide pointing at the form it explains.
10
+ *
11
+ * Link names come free. A link usually points at a page the site also has a
12
+ * record for, so the name shown is that page's own name rather than anchor text
13
+ * nobody extracted. A link to a page with no record keeps its address.
14
+ *
15
+ * Nothing here leaves the origin: a trail level or link on another host is
16
+ * dropped when the graph is built, not when it is walked.
17
+ *
18
+ * @module
19
+ */
20
+ import type { Client } from '@modelcontextprotocol/sdk/client/index.js';
21
+ import type { McpServerInfo, ModelContext, ModelContextTool } from '../../types.js';
22
+ import type { SitePage } from './site-pages.js';
23
+ /** One level of a page's trail. The last is the page itself, and names no URL. */
24
+ export interface TrailLevel {
25
+ /** What the site calls this level. */
26
+ name: string;
27
+ /** Absolute URL, on this origin. Absent on the page's own level. */
28
+ url?: string;
29
+ }
30
+ /** A page's path from the root, root first. */
31
+ export interface SiteTrail {
32
+ /** The page whose trail this is, when it can be told from the record's id. */
33
+ page?: string;
34
+ levels: TrailLevel[];
35
+ }
36
+ /** Somewhere the agent can go from here, and what to call it. */
37
+ export interface NavLink {
38
+ url: string;
39
+ name: string;
40
+ }
41
+ /** What a page is surrounded by. */
42
+ export interface PageNav {
43
+ /** The sections above it, root first, not including the page itself. */
44
+ ancestors: NavLink[];
45
+ /** Pages under the same parent, excluding the page itself. */
46
+ siblings: NavLink[];
47
+ /** Pages whose parent is this page. */
48
+ children: NavLink[];
49
+ /** Pages this page points a reader at. */
50
+ links: NavLink[];
51
+ }
52
+ /** The whole site, ready to answer "where am I and what is near me". */
53
+ export interface SiteNav {
54
+ /** The site's origin, so a graph can be rebuilt from itself. */
55
+ origin: string;
56
+ /** Parent page URL, by page URL. */
57
+ parents: Map<string, string>;
58
+ /** The trail for each page it was published for. */
59
+ trails: Map<string, TrailLevel[]>;
60
+ /** Everything a page points at, by page URL. */
61
+ links: Map<string, string[]>;
62
+ /** Every page known, by URL, for naming. */
63
+ pages: SitePage[];
64
+ }
65
+ /**
66
+ * Read one typed-retrieval record as a trail.
67
+ *
68
+ * @param raw - A `BreadcrumbList` record.
69
+ * @param origin - The site's origin; levels elsewhere are dropped.
70
+ * @returns The trail, or null when the record is not one or carries no levels.
71
+ */
72
+ export declare function parseTrail(raw: unknown, origin: string): SiteTrail | null;
73
+ /**
74
+ * The graph: who is above whom, and what points at what.
75
+ *
76
+ * A trail's parent is its last level with a URL that is not the page itself —
77
+ * the section directly above. A page with a one-level trail (itself) has no
78
+ * parent, which is what the root looks like.
79
+ *
80
+ * @param pages - The site's pages, for names.
81
+ * @param trails - Its trails.
82
+ * @param origin - The site's origin.
83
+ */
84
+ export declare function buildNav(pages: SitePage[], trails: SiteTrail[], origin: string): SiteNav;
85
+ /**
86
+ * Fold trails into a graph already in someone's hands.
87
+ *
88
+ * The tools are built at connect and hold the graph; the trails may arrive
89
+ * later, because fetching them is a read the site's rate limit counts. Mutating
90
+ * in place is what lets a tool built before the fetch answer after it.
91
+ */
92
+ export declare function applyTrails(nav: SiteNav, trails: SiteTrail[]): SiteNav;
93
+ /** What a URL is called: the page's own name, else the last word of its path. */
94
+ export declare function nameOf(url: string, nav: SiteNav): string;
95
+ /** The page that a URL is under, if the graph knows. */
96
+ export declare function parentOf(url: string, nav: SiteNav): string | undefined;
97
+ /**
98
+ * Where the agent can go from a page: up, sideways, down, and out along its
99
+ * own links.
100
+ *
101
+ * @param href - The page to place. Defaults to the browser's.
102
+ */
103
+ export declare function navFor(nav: SiteNav, href: string): PageNav;
104
+ /** Options for {@link loadSiteTrails}. */
105
+ export interface SiteTrailsOptions {
106
+ /** The origin trails must belong to. Defaults to the page's own. */
107
+ origin?: string;
108
+ /** Where the list is cached between page loads. Defaults to `sessionStorage`; null disables. */
109
+ storage?: Storage | null;
110
+ /** How long a cached list is reused, in milliseconds. Defaults to ten minutes. */
111
+ ttlMs?: number;
112
+ /** The clock, for tests. */
113
+ now?: () => number;
114
+ }
115
+ /**
116
+ * Every breadcrumb trail on the site.
117
+ *
118
+ * One query, cached in the tab like the page list, because the hierarchy is the
119
+ * same on every page of it.
120
+ *
121
+ * @throws When the query fails. The caller decides whether that matters.
122
+ */
123
+ export declare function loadSiteTrails(client: Client, info: Pick<McpServerInfo, 'endpoint'>, opts?: SiteTrailsOptions): Promise<SiteTrail[]>;
124
+ /** The tool's registered name. */
125
+ export declare const PAGE_LINKS_TOOL = "page_links";
126
+ /**
127
+ * What the agent can say about where it is.
128
+ *
129
+ * Read rather than navigation: "what else is in this section" is a question,
130
+ * and answering it by opening a page would be answering a different one. The
131
+ * names it returns are the names {@link buildOpenPageTool} accepts, so the
132
+ * model can follow one with the other.
133
+ *
134
+ * Always answers. "Where am I" is answerable from the address bar alone, so a
135
+ * site that publishes no trails and links nothing still gets the page it is on
136
+ * — an agent that cannot say that reads as an agent with no idea where it is.
137
+ */
138
+ export declare function describeNav(nav: SiteNav, href: string): string;
139
+ /** Options for the page-links tool: where the visitor is, and how the graph fills in. */
140
+ export interface PageLinksOptions {
141
+ /** The current page URL. Defaults to the browser's. Read when the tool runs. */
142
+ href?: () => string;
143
+ /**
144
+ * Fetch the site's trails, if they have not been fetched yet.
145
+ *
146
+ * Awaited when the tool runs rather than at connect, because the fetch is a
147
+ * read and a site's rate limit counts reads. Answering "where am I" is what
148
+ * makes that read worth spending.
149
+ */
150
+ loadNav?: () => Promise<SiteNav>;
151
+ }
152
+ /**
153
+ * Build the tool that reports where this page sits and what it leads to.
154
+ *
155
+ * @param nav - The site's graph.
156
+ */
157
+ export declare function buildPageLinksTool(nav: SiteNav, opts?: PageLinksOptions): ModelContextTool;
158
+ /**
159
+ * Register the page-links tool, unless the registry already holds one.
160
+ *
161
+ * Registered whatever the site publishes. It was once gated on the site having
162
+ * trails or links, which withheld the tool from exactly the sites where "where
163
+ * am I" is the only question it could answer.
164
+ *
165
+ * @returns Whether it was registered.
166
+ */
167
+ export declare function registerPageLinksTool(mc: ModelContext, nav: SiteNav, opts?: PageLinksOptions): Promise<boolean>;
168
+ //# sourceMappingURL=site-nav.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"site-nav.d.ts","sourceRoot":"","sources":["../../../src/harness/tools/site-nav.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,316 @@
1
+ import { queryTypedRecords, samePage } from './site-actions.js';
2
+ const SCHEMA_NS = 'https://schema.org/';
3
+ const CACHE_TTL_MS = 10 * 60 * 1000;
4
+ const CACHE_PREFIX = 'nxis:site-trails:v1:';
5
+ const isObj = (v) => !!v && typeof v === 'object' && !Array.isArray(v);
6
+ const schemaValue = (o, prop) => o[prop] ?? o[SCHEMA_NS + prop];
7
+ const str = (v) => typeof v === 'string' && v.trim() ? v.trim() : undefined;
8
+ const bareType = (t) => t.replace(/^.*[/#]/, '');
9
+ const types = (o) => (Array.isArray(o['@type']) ? o['@type'] : [o['@type']])
10
+ .filter((t) => typeof t === 'string')
11
+ .map(bareType);
12
+ /** A URL resolved against the origin, if it stays on it. */
13
+ function onOrigin(raw, origin) {
14
+ const s = str(raw) ?? (isObj(raw) ? str(raw['@id']) ?? str(schemaValue(raw, 'url')) : undefined);
15
+ if (!s)
16
+ return undefined;
17
+ try {
18
+ const url = new URL(s, origin);
19
+ url.hash = '';
20
+ return url.origin === new URL(origin).origin ? url.href : undefined;
21
+ }
22
+ catch {
23
+ return undefined;
24
+ }
25
+ }
26
+ /** The page a trail was published for: its record's id, without the fragment. */
27
+ function pageOfTrail(raw, origin) {
28
+ const id = str(raw['@id']);
29
+ if (!id || !id.includes('#'))
30
+ return undefined;
31
+ return onOrigin(id, origin);
32
+ }
33
+ /**
34
+ * Read one typed-retrieval record as a trail.
35
+ *
36
+ * @param raw - A `BreadcrumbList` record.
37
+ * @param origin - The site's origin; levels elsewhere are dropped.
38
+ * @returns The trail, or null when the record is not one or carries no levels.
39
+ */
40
+ export function parseTrail(raw, origin) {
41
+ if (!isObj(raw) || !types(raw).includes('BreadcrumbList'))
42
+ return null;
43
+ const rawLevels = schemaValue(raw, 'itemListElement');
44
+ const list = Array.isArray(rawLevels) ? rawLevels : rawLevels ? [rawLevels] : [];
45
+ const levels = list
46
+ .filter(isObj)
47
+ .map((item) => {
48
+ const position = Number(schemaValue(item, 'position') ?? Number.MAX_SAFE_INTEGER);
49
+ const name = str(schemaValue(item, 'name'));
50
+ const url = onOrigin(schemaValue(item, 'item'), origin);
51
+ return { position, name, url };
52
+ })
53
+ .filter((l) => !!l.name)
54
+ .sort((a, b) => a.position - b.position)
55
+ .map(({ name, url }) => (url ? { name, url } : { name }));
56
+ if (!levels.length)
57
+ return null;
58
+ const page = pageOfTrail(raw, origin);
59
+ return page ? { page, levels } : { levels };
60
+ }
61
+ /**
62
+ * The graph: who is above whom, and what points at what.
63
+ *
64
+ * A trail's parent is its last level with a URL that is not the page itself —
65
+ * the section directly above. A page with a one-level trail (itself) has no
66
+ * parent, which is what the root looks like.
67
+ *
68
+ * @param pages - The site's pages, for names.
69
+ * @param trails - Its trails.
70
+ * @param origin - The site's origin.
71
+ */
72
+ export function buildNav(pages, trails, origin) {
73
+ const parents = new Map();
74
+ const byPage = new Map();
75
+ const links = new Map();
76
+ for (const trail of trails) {
77
+ // A trail with no id to place it still names its page in its last level.
78
+ const page = trail.page ?? trail.levels[trail.levels.length - 1]?.url;
79
+ if (!page)
80
+ continue;
81
+ byPage.set(page, trail.levels);
82
+ const above = trail.levels.filter((l) => l.url && !samePage(l.url, page));
83
+ const parent = above[above.length - 1]?.url;
84
+ if (parent)
85
+ parents.set(page, parent);
86
+ }
87
+ for (const page of pages) {
88
+ const out = (page.links ?? [])
89
+ .map((l) => onOrigin(l, origin))
90
+ .filter((l) => !!l && !samePage(l, page.url));
91
+ if (out.length)
92
+ links.set(page.url, [...new Set(out)]);
93
+ }
94
+ return { origin, parents, trails: byPage, links, pages };
95
+ }
96
+ /**
97
+ * Fold trails into a graph already in someone's hands.
98
+ *
99
+ * The tools are built at connect and hold the graph; the trails may arrive
100
+ * later, because fetching them is a read the site's rate limit counts. Mutating
101
+ * in place is what lets a tool built before the fetch answer after it.
102
+ */
103
+ export function applyTrails(nav, trails) {
104
+ const fresh = buildNav(nav.pages, trails, nav.origin);
105
+ nav.parents.clear();
106
+ for (const [page, parent] of fresh.parents)
107
+ nav.parents.set(page, parent);
108
+ nav.trails.clear();
109
+ for (const [page, levels] of fresh.trails)
110
+ nav.trails.set(page, levels);
111
+ return nav;
112
+ }
113
+ /** What a URL is called: the page's own name, else the last word of its path. */
114
+ export function nameOf(url, nav) {
115
+ const known = nav.pages.find((p) => samePage(p.url, url));
116
+ if (known)
117
+ return known.name;
118
+ const level = [...nav.trails.values()].flat().find((l) => l.url && samePage(l.url, url));
119
+ if (level)
120
+ return level.name;
121
+ try {
122
+ const slug = new URL(url).pathname.split('/').filter(Boolean).pop() ?? '';
123
+ return slug.replace(/\.\w+$/, '').replace(/[-_]+/g, ' ') || 'home';
124
+ }
125
+ catch {
126
+ return url;
127
+ }
128
+ }
129
+ /** The page that a URL is under, if the graph knows. */
130
+ export function parentOf(url, nav) {
131
+ const key = [...nav.parents.keys()].find((p) => samePage(p, url));
132
+ return key ? nav.parents.get(key) : undefined;
133
+ }
134
+ /**
135
+ * Where the agent can go from a page: up, sideways, down, and out along its
136
+ * own links.
137
+ *
138
+ * @param href - The page to place. Defaults to the browser's.
139
+ */
140
+ export function navFor(nav, href) {
141
+ const link = (url) => ({ url, name: nameOf(url, nav) });
142
+ const ancestors = [];
143
+ let up = parentOf(href, nav);
144
+ // A cycle in published trails would otherwise walk forever.
145
+ while (up && ancestors.length < 8 && !ancestors.some((a) => samePage(a.url, up))) {
146
+ ancestors.unshift(link(up));
147
+ up = parentOf(up, nav);
148
+ }
149
+ const parent = parentOf(href, nav);
150
+ const siblings = parent
151
+ ? [...nav.parents.entries()]
152
+ .filter(([page, under]) => samePage(under, parent) && !samePage(page, href))
153
+ .map(([page]) => link(page))
154
+ : [];
155
+ const children = [...nav.parents.entries()]
156
+ .filter(([, under]) => samePage(under, href))
157
+ .map(([page]) => link(page));
158
+ const own = [...nav.links.keys()].find((p) => samePage(p, href));
159
+ const links = (own ? (nav.links.get(own) ?? []) : []).map(link);
160
+ return { ancestors, siblings, children, links };
161
+ }
162
+ function defaultStorage() {
163
+ try {
164
+ return typeof sessionStorage === 'undefined' ? null : sessionStorage;
165
+ }
166
+ catch {
167
+ return null;
168
+ }
169
+ }
170
+ /**
171
+ * Every breadcrumb trail on the site.
172
+ *
173
+ * One query, cached in the tab like the page list, because the hierarchy is the
174
+ * same on every page of it.
175
+ *
176
+ * @throws When the query fails. The caller decides whether that matters.
177
+ */
178
+ export async function loadSiteTrails(client, info, opts = {}) {
179
+ const origin = opts.origin ?? location.origin;
180
+ const storage = opts.storage === undefined ? defaultStorage() : opts.storage;
181
+ const now = (opts.now ?? Date.now)();
182
+ const key = CACHE_PREFIX + info.endpoint;
183
+ try {
184
+ const entry = JSON.parse(storage?.getItem(key) || 'null');
185
+ const fresh = entry && now - entry.fetchedAt <= (opts.ttlMs ?? CACHE_TTL_MS) && entry.fetchedAt <= now;
186
+ if (fresh && Array.isArray(entry.trails))
187
+ return entry.trails;
188
+ }
189
+ catch {
190
+ // A corrupt entry is a refetch.
191
+ }
192
+ const trails = (await queryTypedRecords(client, 'BreadcrumbList'))
193
+ .map((r) => parseTrail(r, origin))
194
+ .filter((t) => t !== null);
195
+ try {
196
+ storage?.setItem(key, JSON.stringify({ fetchedAt: now, trails }));
197
+ }
198
+ catch {
199
+ // Full or blocked storage costs a refetch on the next page, nothing more.
200
+ }
201
+ return trails;
202
+ }
203
+ /** The tool's registered name. */
204
+ export const PAGE_LINKS_TOOL = 'page_links';
205
+ /** Where a listing stops, so an answer stays readable. */
206
+ const MAX_LISTED = 10;
207
+ const list = (label, links) => links.length ? [`${label}: ${links.slice(0, MAX_LISTED).map((l) => l.name).join(', ')}`] : [];
208
+ /**
209
+ * What the agent can say about where it is.
210
+ *
211
+ * Read rather than navigation: "what else is in this section" is a question,
212
+ * and answering it by opening a page would be answering a different one. The
213
+ * names it returns are the names {@link buildOpenPageTool} accepts, so the
214
+ * model can follow one with the other.
215
+ *
216
+ * Always answers. "Where am I" is answerable from the address bar alone, so a
217
+ * site that publishes no trails and links nothing still gets the page it is on
218
+ * — an agent that cannot say that reads as an agent with no idea where it is.
219
+ */
220
+ export function describeNav(nav, href) {
221
+ const { ancestors, siblings, children, links } = navFor(nav, href);
222
+ const trail = [...nav.trails.entries()].find(([page]) => samePage(page, href))?.[1];
223
+ const known = nav.pages.length;
224
+ const lines = [
225
+ `You are on ${nameOf(href, nav)} (${pathOf(href, nav)})` +
226
+ (known ? `, one of ${known} page${known === 1 ? '' : 's'} on this site.` : '.'),
227
+ ...(trail?.length ? [`Trail: ${trail.map((l) => l.name).join(' › ')}`] : []),
228
+ ...list('Above', ancestors),
229
+ ...list('Also in this section', siblings),
230
+ ...list('Under this page', children),
231
+ ...list('Linked from here', links),
232
+ ];
233
+ if (lines.length > 1)
234
+ return lines.join('\n');
235
+ const elsewhere = nav.pages.filter((p) => !samePage(p.url, href)).slice(0, MAX_LISTED);
236
+ return elsewhere.length
237
+ ? `${lines[0]} This site publishes no sections, so there is nothing above or beside it. Other pages: ${elsewhere
238
+ .map((p) => p.name)
239
+ .join(', ')}.`
240
+ : `${lines[0]} Nothing else on the site links to or from it.`;
241
+ }
242
+ /**
243
+ * The address as a visitor reads it, which is what "what page is this" asks for.
244
+ *
245
+ * The path of the page the site actually publishes, when one matches. A query a surface put there
246
+ * — `?surface=window`, `?open=1` — belongs to this tab, not to the site: nothing is indexed under
247
+ * it, and quoting it back invites the model to ask for a page by an address no record carries.
248
+ */
249
+ function pathOf(href, nav) {
250
+ const known = nav.pages.find((p) => samePage(p.url, href));
251
+ try {
252
+ // A published page's own query is part of its address; the tab's is not, and with no record to
253
+ // say otherwise, the path alone is the page.
254
+ if (known) {
255
+ const url = new URL(known.url);
256
+ return url.pathname + url.search;
257
+ }
258
+ return new URL(href).pathname;
259
+ }
260
+ catch {
261
+ return known?.url ?? href;
262
+ }
263
+ }
264
+ /**
265
+ * Build the tool that reports where this page sits and what it leads to.
266
+ *
267
+ * @param nav - The site's graph.
268
+ */
269
+ export function buildPageLinksTool(nav, opts = {}) {
270
+ return {
271
+ name: PAGE_LINKS_TOOL,
272
+ description: 'Where the visitor is right now and what this page leads to: the page they are on, the sections ' +
273
+ 'above it, the other pages in its section, the pages under it, and the pages it links to. Use for ' +
274
+ '"where am I", "what page is this" and "what else is here", and before offering to take them somewhere.',
275
+ inputSchema: { type: 'object', properties: {} },
276
+ annotations: { readOnlyHint: true },
277
+ execute: async () => {
278
+ const filled = (await opts.loadNav?.().catch(() => nav)) ?? nav;
279
+ return {
280
+ content: [
281
+ {
282
+ type: 'text',
283
+ text: describeNav(filled, opts.href?.() ?? (typeof location === 'undefined' ? '' : location.href)),
284
+ },
285
+ ],
286
+ };
287
+ },
288
+ };
289
+ }
290
+ /**
291
+ * Register the page-links tool, unless the registry already holds one.
292
+ *
293
+ * Registered whatever the site publishes. It was once gated on the site having
294
+ * trails or links, which withheld the tool from exactly the sites where "where
295
+ * am I" is the only question it could answer.
296
+ *
297
+ * @returns Whether it was registered.
298
+ */
299
+ export async function registerPageLinksTool(mc, nav, opts = {}) {
300
+ try {
301
+ if ((await mc.getTools()).some((t) => t.name === PAGE_LINKS_TOOL))
302
+ return false;
303
+ }
304
+ catch {
305
+ // A registry that cannot list may still register.
306
+ }
307
+ try {
308
+ await mc.registerTool(buildPageLinksTool(nav, opts));
309
+ return true;
310
+ }
311
+ catch (err) {
312
+ console.warn(`[nxis-mcp] could not register ${PAGE_LINKS_TOOL}:`, err);
313
+ return false;
314
+ }
315
+ }
316
+ //# sourceMappingURL=site-nav.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"site-nav.js","sourceRoot":"","sources":["../../../src/harness/tools/site-nav.ts"],"names":[],"mappings":"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"}
@@ -23,6 +23,10 @@ export interface SitePage {
23
23
  name: string;
24
24
  /** The `@id`s of the records the page is about, when its record says. */
25
25
  about?: string[];
26
+ /** Pages this one points a reader at: its `significantLink` and `relatedLink`. */
27
+ links?: string[];
28
+ /** The `@id` of the breadcrumb trail extracted from this page, when it has one. */
29
+ breadcrumb?: string;
26
30
  }
27
31
  type Obj = Record<string, unknown>;
28
32
  /**
@@ -51,10 +55,11 @@ export declare function mergePages(...lists: SitePage[][]): SitePage[];
51
55
  /**
52
56
  * The page a record came from, when it can be told.
53
57
  *
54
- * In order of how much each is to be trusted: the record naming its page, the
55
- * record being a page, the record's own URL when it is a page of this site
56
- * rather than its root, a page that says it is about the record, and last a
57
- * page with the record's exact name.
58
+ * In order of how much each is to be trusted: the record naming its page, its
59
+ * id when that is formed from a page (`page.html#organization`), the record
60
+ * being a page, the record's own URL when it is a page of this site rather than
61
+ * its root, a page that says it is about the record, and last a page with the
62
+ * record's exact name.
58
63
  *
59
64
  * @param entity - The record, compact or IRI-keyed.
60
65
  * @param pages - The site's pages.
@@ -1 +1 @@
1
- {"version":3,"file":"site-pages.d.ts","sourceRoot":"","sources":["../../../src/harness/tools/site-pages.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AACH,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,2CAA2C,CAAC;AACxE,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,gBAAgB,CAAC;AACpD,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,0BAA0B,CAAC;AAO3D,oDAAoD;AACpD,MAAM,WAAW,QAAQ;IACvB,oCAAoC;IACpC,GAAG,EAAE,MAAM,CAAC;IACZ,8DAA8D;IAC9D,IAAI,EAAE,MAAM,CAAC;IACb,yEAAyE;IACzE,KAAK,CAAC,EAAE,MAAM,EAAE,CAAC;CAClB;AAED,KAAK,GAAG,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;AAoCnC;;;;;;;GAOG;AACH,wBAAgB,SAAS,CAAC,GAAG,EAAE,OAAO,EAAE,MAAM,EAAE,MAAM,GAAG,QAAQ,GAAG,IAAI,CAOvE;AAED;;;;;GAKG;AACH,wBAAgB,gBAAgB,CAAC,OAAO,EAAE,UAAU,EAAE,EAAE,MAAM,EAAE,MAAM,GAAG,QAAQ,EAAE,CAUlF;AAQD;;;;;GAKG;AACH,wBAAgB,UAAU,CAAC,GAAG,KAAK,EAAE,QAAQ,EAAE,EAAE,GAAG,QAAQ,EAAE,CAQ7D;AAED;;;;;;;;;;;;GAYG;AACH,wBAAgB,aAAa,CAAC,MAAM,EAAE,GAAG,EAAE,KAAK,EAAE,QAAQ,EAAE,EAAE,MAAM,EAAE,MAAM,GAAG,QAAQ,GAAG,IAAI,CA2B7F;AAED;;;;;;;GAOG;AACH,wBAAgB,QAAQ,CAAC,MAAM,EAAE,MAAM,EAAE,KAAK,EAAE,QAAQ,EAAE,EAAE,MAAM,EAAE,MAAM,GAAG,QAAQ,GAAG,IAAI,CAY3F;AAED,yCAAyC;AACzC,MAAM,WAAW,gBAAgB;IAC/B,mEAAmE;IACnE,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,gGAAgG;IAChG,OAAO,CAAC,EAAE,OAAO,GAAG,IAAI,CAAC;IACzB,kFAAkF;IAClF,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,4BAA4B;IAC5B,GAAG,CAAC,EAAE,MAAM,MAAM,CAAC;CACpB;AAUD;;;;;;;GAOG;AACH,wBAAsB,aAAa,CACjC,MAAM,EAAE,MAAM,EACd,IAAI,EAAE,IAAI,CAAC,aAAa,EAAE,UAAU,CAAC,EACrC,IAAI,GAAE,gBAAqB,GAC1B,OAAO,CAAC,QAAQ,EAAE,CAAC,CAyBrB"}
1
+ {"version":3,"file":"site-pages.d.ts","sourceRoot":"","sources":["../../../src/harness/tools/site-pages.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AACH,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,2CAA2C,CAAC;AACxE,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,gBAAgB,CAAC;AACpD,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,0BAA0B,CAAC;AAO3D,oDAAoD;AACpD,MAAM,WAAW,QAAQ;IACvB,oCAAoC;IACpC,GAAG,EAAE,MAAM,CAAC;IACZ,8DAA8D;IAC9D,IAAI,EAAE,MAAM,CAAC;IACb,yEAAyE;IACzE,KAAK,CAAC,EAAE,MAAM,EAAE,CAAC;IACjB,kFAAkF;IAClF,KAAK,CAAC,EAAE,MAAM,EAAE,CAAC;IACjB,mFAAmF;IACnF,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB;AAED,KAAK,GAAG,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;AAoCnC;;;;;;;GAOG;AACH,wBAAgB,SAAS,CAAC,GAAG,EAAE,OAAO,EAAE,MAAM,EAAE,MAAM,GAAG,QAAQ,GAAG,IAAI,CAiBvE;AAED;;;;;GAKG;AACH,wBAAgB,gBAAgB,CAAC,OAAO,EAAE,UAAU,EAAE,EAAE,MAAM,EAAE,MAAM,GAAG,QAAQ,EAAE,CAUlF;AAQD;;;;;GAKG;AACH,wBAAgB,UAAU,CAAC,GAAG,KAAK,EAAE,QAAQ,EAAE,EAAE,GAAG,QAAQ,EAAE,CAY7D;AAED;;;;;;;;;;;;;GAaG;AACH,wBAAgB,aAAa,CAAC,MAAM,EAAE,GAAG,EAAE,KAAK,EAAE,QAAQ,EAAE,EAAE,MAAM,EAAE,MAAM,GAAG,QAAQ,GAAG,IAAI,CAiC7F;AAiBD;;;;;;;GAOG;AACH,wBAAgB,QAAQ,CAAC,MAAM,EAAE,MAAM,EAAE,KAAK,EAAE,QAAQ,EAAE,EAAE,MAAM,EAAE,MAAM,GAAG,QAAQ,GAAG,IAAI,CAY3F;AAED,yCAAyC;AACzC,MAAM,WAAW,gBAAgB;IAC/B,mEAAmE;IACnE,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,gGAAgG;IAChG,OAAO,CAAC,EAAE,OAAO,GAAG,IAAI,CAAC;IACzB,kFAAkF;IAClF,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,4BAA4B;IAC5B,GAAG,CAAC,EAAE,MAAM,MAAM,CAAC;CACpB;AAUD;;;;;;;GAOG;AACH,wBAAsB,aAAa,CACjC,MAAM,EAAE,MAAM,EACd,IAAI,EAAE,IAAI,CAAC,aAAa,EAAE,UAAU,CAAC,EACrC,IAAI,GAAE,gBAAqB,GAC1B,OAAO,CAAC,QAAQ,EAAE,CAAC,CAyBrB"}
@@ -49,7 +49,17 @@ export function parsePage(raw, origin) {
49
49
  if (!url || !name)
50
50
  return null;
51
51
  const about = [...refs(schemaValue(raw, 'mainEntity')), ...refs(schemaValue(raw, 'about'))];
52
- return about.length ? { url, name, about } : { url, name };
52
+ // What the page points at, which is how an agent follows a page's own links rather than only
53
+ // the hierarchy above it. Kept as published; the platform stores them absolute.
54
+ const links = [...new Set([...refs(schemaValue(raw, 'significantLink')), ...refs(schemaValue(raw, 'relatedLink'))])];
55
+ const breadcrumb = refs(schemaValue(raw, 'breadcrumb'))[0];
56
+ return {
57
+ url,
58
+ name,
59
+ ...(about.length ? { about } : {}),
60
+ ...(links.length ? { links } : {}),
61
+ ...(breadcrumb ? { breadcrumb } : {}),
62
+ };
53
63
  }
54
64
  /**
55
65
  * The pages the site's actions live on, named for what each is about.
@@ -87,18 +97,25 @@ export function mergePages(...lists) {
87
97
  const same = out.find((p) => samePage(p.url, page.url));
88
98
  if (!same)
89
99
  out.push({ ...page });
90
- else if (page.about?.length)
91
- same.about = [...new Set([...(same.about ?? []), ...page.about])];
100
+ else {
101
+ if (page.about?.length)
102
+ same.about = [...new Set([...(same.about ?? []), ...page.about])];
103
+ if (page.links?.length)
104
+ same.links = [...new Set([...(same.links ?? []), ...page.links])];
105
+ if (page.breadcrumb && !same.breadcrumb)
106
+ same.breadcrumb = page.breadcrumb;
107
+ }
92
108
  }
93
109
  return out;
94
110
  }
95
111
  /**
96
112
  * The page a record came from, when it can be told.
97
113
  *
98
- * In order of how much each is to be trusted: the record naming its page, the
99
- * record being a page, the record's own URL when it is a page of this site
100
- * rather than its root, a page that says it is about the record, and last a
101
- * page with the record's exact name.
114
+ * In order of how much each is to be trusted: the record naming its page, its
115
+ * id when that is formed from a page (`page.html#organization`), the record
116
+ * being a page, the record's own URL when it is a page of this site rather than
117
+ * its root, a page that says it is about the record, and last a page with the
118
+ * record's exact name.
102
119
  *
103
120
  * @param entity - The record, compact or IRI-keyed.
104
121
  * @param pages - The site's pages.
@@ -115,6 +132,12 @@ export function pageForEntity(entity, pages, origin) {
115
132
  if (url)
116
133
  return known(url);
117
134
  }
135
+ // An id built from its page (`page.html#organization`) names the page: everything before the
136
+ // fragment. Only with a fragment and below the root: a site-wide id (`/#organization/...`) names
137
+ // no page, and the hash-suffixed ids stored before (`page.html/23400b...`) are not pages either.
138
+ const idPage = pageOfId(str(entity['@id']), origin);
139
+ if (idPage)
140
+ return known(idPage);
118
141
  const own = onOrigin(schemaValue(entity, 'url'), origin);
119
142
  if (own && (types(entity).some((t) => t.endsWith('Page')) || new URL(own).pathname !== '/')) {
120
143
  return known(own);
@@ -132,6 +155,22 @@ export function pageForEntity(entity, pages, origin) {
132
155
  }
133
156
  return null;
134
157
  }
158
+ /**
159
+ * The page an id formed from it names, or null.
160
+ *
161
+ * nxis-sdn-api forms a page entity's id as the page URL plus a fragment, so the page is the id with
162
+ * the fragment removed.
163
+ */
164
+ function pageOfId(id, origin) {
165
+ if (!id || !id.includes('#'))
166
+ return null;
167
+ const url = onOrigin(id, origin);
168
+ if (!url)
169
+ return null;
170
+ const page = new URL(url);
171
+ page.hash = '';
172
+ return page.pathname === '/' ? null : page.href;
173
+ }
135
174
  /**
136
175
  * The page a visitor or model means, by name or by address.
137
176
  *