@aibranch/llm-meta-widget 0.8.5 → 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.
|
@@ -483,6 +483,17 @@ async function readMcpResource({ endpoint, uri, signal }) {
|
|
|
483
483
|
label: `readMcpResource(${uri})`
|
|
484
484
|
});
|
|
485
485
|
}
|
|
486
|
+
async function getPagePrompt({ name, args, prompts }) {
|
|
487
|
+
const handler = prompts && prompts[name];
|
|
488
|
+
if (typeof handler !== "function") {
|
|
489
|
+
throw new Error('the page declares prompt "' + name + '" but no handler answers to that name');
|
|
490
|
+
}
|
|
491
|
+
const result = await handler(args || {});
|
|
492
|
+
if (typeof result === "string") {
|
|
493
|
+
return { messages: [{ role: "user", content: { type: "text", text: result } }] };
|
|
494
|
+
}
|
|
495
|
+
return result || { messages: [] };
|
|
496
|
+
}
|
|
486
497
|
function promptMessagesToText(result) {
|
|
487
498
|
return (result?.messages || []).map((m) => {
|
|
488
499
|
const c = m.content;
|
|
@@ -549,24 +560,71 @@ function parseSseFrame(raw) {
|
|
|
549
560
|
return { name, data };
|
|
550
561
|
}
|
|
551
562
|
var STATIC_PRIMITIVES_META = "io.modelcontextprotocol/static-primitives";
|
|
563
|
+
function readStaticPrimitivesCard(text, warn = console.warn) {
|
|
564
|
+
const empty = { tools: [], prompts: [], resources: [] };
|
|
565
|
+
if (typeof text !== "string" || text.trim() === "") return empty;
|
|
566
|
+
let doc;
|
|
567
|
+
try {
|
|
568
|
+
doc = JSON.parse(text);
|
|
569
|
+
} catch (e) {
|
|
570
|
+
warn("[llm-meta-widget] page card is not valid JSON; ignoring it:", e.message);
|
|
571
|
+
return empty;
|
|
572
|
+
}
|
|
573
|
+
const ext = doc && doc._meta && doc._meta[STATIC_PRIMITIVES_META] || null;
|
|
574
|
+
if (!ext) {
|
|
575
|
+
warn(`[llm-meta-widget] page card has no _meta["${STATIC_PRIMITIVES_META}"]; ignoring it`);
|
|
576
|
+
return empty;
|
|
577
|
+
}
|
|
578
|
+
const named = (arr, kind) => (Array.isArray(arr) ? arr : []).filter((e) => {
|
|
579
|
+
const ok = e && typeof e.name === "string" && e.name.trim() !== "";
|
|
580
|
+
if (!ok) warn(`[llm-meta-widget] page card: dropping a ${kind} with no name`);
|
|
581
|
+
return ok;
|
|
582
|
+
});
|
|
583
|
+
return {
|
|
584
|
+
tools: named(ext.tools, "tool"),
|
|
585
|
+
prompts: named(ext.prompts, "prompt"),
|
|
586
|
+
// Resources are addressed by uri, not name — resources/read takes the uri.
|
|
587
|
+
resources: (Array.isArray(ext.resources) ? ext.resources : []).filter((e) => {
|
|
588
|
+
const ok = e && typeof e.uri === "string" && e.uri.trim() !== "";
|
|
589
|
+
if (!ok) warn("[llm-meta-widget] page card: dropping a resource with no uri");
|
|
590
|
+
return ok;
|
|
591
|
+
})
|
|
592
|
+
};
|
|
593
|
+
}
|
|
552
594
|
var VOLATILITIES = ["stable", "volatile"];
|
|
595
|
+
var usableSize = (n) => typeof n === "number" && isFinite(n) ? n : null;
|
|
596
|
+
var pickDeclared = (own, fromMeta) => own === void 0 ? fromMeta : own;
|
|
553
597
|
function resourceHints(entry) {
|
|
554
598
|
const meta = entry && entry._meta && entry._meta[STATIC_PRIMITIVES_META] || {};
|
|
555
599
|
return {
|
|
556
600
|
// A non-numeric or absent size means "unknown", never 0 — 0 would read
|
|
557
601
|
// as a free resource and sail through every budget check.
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
602
|
+
// Base `size` wins; `_meta.sizeBytes` is the earlier draft's spelling and
|
|
603
|
+
// is accepted so a new client keeps its budget gate against an old server.
|
|
604
|
+
size: usableSize(entry && entry.size) ?? usableSize(meta.sizeBytes),
|
|
605
|
+
// Deprecated alias, kept for one release so renaming the field is not a
|
|
606
|
+
// breaking change: orchestrator.js is a published entry point, so anything
|
|
607
|
+
// it returns is public API. Dropped at 0.9.0 together with the
|
|
608
|
+
// `_meta.sizeBytes` fallback above — one deprecation, one bump.
|
|
609
|
+
get sizeBytes() {
|
|
610
|
+
return this.size;
|
|
611
|
+
},
|
|
612
|
+
// Same rule as `size`: the entry's own field wins, `_meta` is the fallback.
|
|
613
|
+
// SEP-0000 defines these ON the Resource object; a server puts them in
|
|
614
|
+
// `_meta` only because the extension is not merged yet. A PAGE card's
|
|
615
|
+
// entries already sit inside the extension block, so for them the fields
|
|
616
|
+
// are simply top-level — reading both is what lets one planner serve both.
|
|
617
|
+
volatility: VOLATILITIES.indexOf(pickDeclared(entry && entry.volatility, meta.volatility)) === -1 ? "stable" : pickDeclared(entry && entry.volatility, meta.volatility),
|
|
618
|
+
autoAttach: typeof pickDeclared(entry && entry.autoAttach, meta.autoAttach) === "boolean" ? pickDeclared(entry && entry.autoAttach, meta.autoAttach) : true
|
|
561
619
|
};
|
|
562
620
|
}
|
|
563
621
|
function planResourceAttachment(entry, budgetBytes) {
|
|
564
|
-
const {
|
|
565
|
-
const base = { sizeBytes, volatility, uri: entry && entry.uri };
|
|
622
|
+
const { size, volatility, autoAttach } = resourceHints(entry);
|
|
623
|
+
const base = { size, sizeBytes: size, volatility, uri: entry && entry.uri };
|
|
566
624
|
if (!autoAttach) {
|
|
567
625
|
return { ...base, fetch: false, autoAttach: false, reason: "not-auto-attach" };
|
|
568
626
|
}
|
|
569
|
-
if (
|
|
627
|
+
if (size !== null && size > budgetBytes) {
|
|
570
628
|
return { ...base, fetch: false, autoAttach: false, reason: "over-budget" };
|
|
571
629
|
}
|
|
572
630
|
return {
|
|
@@ -575,7 +633,7 @@ function planResourceAttachment(entry, budgetBytes) {
|
|
|
575
633
|
autoAttach: true,
|
|
576
634
|
// Unknown size still gets fetched, then trimmed — the trim is the
|
|
577
635
|
// safety net for servers that do not declare a size.
|
|
578
|
-
reason:
|
|
636
|
+
reason: size === null ? "size-unknown" : "within-budget"
|
|
579
637
|
};
|
|
580
638
|
}
|
|
581
639
|
function trimResourceText(text, maxBytes) {
|
|
@@ -601,6 +659,45 @@ async function readResourceContext({ endpoint, uri, name, read, budgetBytes }) {
|
|
|
601
659
|
return null;
|
|
602
660
|
}
|
|
603
661
|
}
|
|
662
|
+
function planPageResources({ entries, budgetBytes, onSkip }) {
|
|
663
|
+
return (entries || []).map((entry) => {
|
|
664
|
+
const plan = planResourceAttachment(entry, budgetBytes);
|
|
665
|
+
plan.name = entry.name || entry.uri;
|
|
666
|
+
plan.description = entry.description || "";
|
|
667
|
+
if (!plan.fetch && onSkip) onSkip(plan);
|
|
668
|
+
return plan;
|
|
669
|
+
}).filter((plan) => plan.fetch);
|
|
670
|
+
}
|
|
671
|
+
function pageResourceLine(plan, text) {
|
|
672
|
+
return "- " + plan.name + (plan.description ? " (" + plan.description + ")" : "") + ": " + text;
|
|
673
|
+
}
|
|
674
|
+
async function pageResourceLinesForTurn({ plans, resources, budgetBytes, cache = {} }) {
|
|
675
|
+
const lines = [];
|
|
676
|
+
for (const plan of plans || []) {
|
|
677
|
+
let text = plan.volatility === "volatile" ? null : cache[plan.uri];
|
|
678
|
+
if (text === null || text === void 0) {
|
|
679
|
+
text = await readPageResource({ uri: plan.uri, resources, budgetBytes });
|
|
680
|
+
if (plan.volatility !== "volatile" && text !== null) cache[plan.uri] = text;
|
|
681
|
+
}
|
|
682
|
+
if (text !== null) lines.push(pageResourceLine(plan, text));
|
|
683
|
+
}
|
|
684
|
+
return { lines, cache };
|
|
685
|
+
}
|
|
686
|
+
async function readPageResource({ uri, resources, budgetBytes }) {
|
|
687
|
+
const handler = resources && resources[uri];
|
|
688
|
+
if (typeof handler !== "function") {
|
|
689
|
+
console.warn('[llm-meta-widget] the page declares resource "' + uri + '" but no handler answers to that uri');
|
|
690
|
+
return null;
|
|
691
|
+
}
|
|
692
|
+
try {
|
|
693
|
+
const value = await handler();
|
|
694
|
+
const text = typeof value === "string" ? value : JSON.stringify(value);
|
|
695
|
+
return trimResourceText(text === void 0 ? "" : text, budgetBytes);
|
|
696
|
+
} catch (e) {
|
|
697
|
+
console.warn('[llm-meta-widget] reading "' + uri + '" failed:', e && e.message);
|
|
698
|
+
return null;
|
|
699
|
+
}
|
|
700
|
+
}
|
|
604
701
|
async function loadHostResource({ endpoint, budgetBytes, list: list2, read, onSkip }) {
|
|
605
702
|
const resources = await list2({ endpoint });
|
|
606
703
|
const candidate = (resources || []).filter((r) => (r.mimeType || "") === "application/json")[0];
|
|
@@ -2894,8 +2991,11 @@ var lexer = _Lexer.lex;
|
|
|
2894
2991
|
var DEFAULTS = Object.freeze({
|
|
2895
2992
|
API_KEY_UUID: "ollama-local",
|
|
2896
2993
|
ACTIONS_SCHEMA_ID: "ai-actions",
|
|
2994
|
+
PAGE_CARD_ID: "ai-page",
|
|
2897
2995
|
STATE_GLOBAL: "aiState",
|
|
2898
2996
|
ACTIONS_GLOBAL: "aiActions",
|
|
2997
|
+
PROMPTS_GLOBAL: "aiPrompts",
|
|
2998
|
+
RESOURCES_GLOBAL: "aiResources",
|
|
2899
2999
|
REMOTE_TOOLS_SCHEMA_ID: "remote-mcp-tools",
|
|
2900
3000
|
LLM_PROVIDER: "llm_meta_server",
|
|
2901
3001
|
MAX_ROUNDS: 3
|
|
@@ -2929,9 +3029,12 @@ function readConfig(el) {
|
|
|
2929
3029
|
API_KEY_UUID: str("api-key-uuid", DEFAULTS.API_KEY_UUID),
|
|
2930
3030
|
MODEL: str("model", null),
|
|
2931
3031
|
ACTIONS_SCHEMA_ID: str("actions-schema-id", DEFAULTS.ACTIONS_SCHEMA_ID),
|
|
3032
|
+
PAGE_CARD_ID: str("page-card-id", DEFAULTS.PAGE_CARD_ID),
|
|
2932
3033
|
GREETING: str("greeting", null),
|
|
2933
3034
|
STATE_GLOBAL: str("state-global", DEFAULTS.STATE_GLOBAL),
|
|
2934
3035
|
ACTIONS_GLOBAL: str("actions-global", DEFAULTS.ACTIONS_GLOBAL),
|
|
3036
|
+
PROMPTS_GLOBAL: str("prompts-global", DEFAULTS.PROMPTS_GLOBAL),
|
|
3037
|
+
RESOURCES_GLOBAL: str("resources-global", DEFAULTS.RESOURCES_GLOBAL),
|
|
2935
3038
|
REMOTE_TOOLS_SCHEMA_ID: str("remote-tools-schema-id", DEFAULTS.REMOTE_TOOLS_SCHEMA_ID),
|
|
2936
3039
|
MAX_ROUNDS: int("max-rounds", DEFAULTS.MAX_ROUNDS),
|
|
2937
3040
|
WELL_KNOWN_URLS: wellKnown === null ? null : wellKnown.trim() === "" ? [] : splitList(wellKnown),
|
|
@@ -3648,9 +3751,12 @@ function boot(cfg) {
|
|
|
3648
3751
|
var API_KEY_UUID = cfg.API_KEY_UUID;
|
|
3649
3752
|
var MODEL = cfg.MODEL;
|
|
3650
3753
|
var ACTIONS_SCHEMA_ID = cfg.ACTIONS_SCHEMA_ID;
|
|
3754
|
+
var PAGE_CARD_ID = cfg.PAGE_CARD_ID;
|
|
3651
3755
|
var GREETING = cfg.GREETING;
|
|
3652
3756
|
var STATE_GLOBAL = cfg.STATE_GLOBAL;
|
|
3653
3757
|
var ACTIONS_GLOBAL = cfg.ACTIONS_GLOBAL;
|
|
3758
|
+
var PROMPTS_GLOBAL = cfg.PROMPTS_GLOBAL;
|
|
3759
|
+
var RESOURCES_GLOBAL = cfg.RESOURCES_GLOBAL;
|
|
3654
3760
|
var REMOTE_TOOLS_SCHEMA_ID = cfg.REMOTE_TOOLS_SCHEMA_ID;
|
|
3655
3761
|
var MAX_ROUNDS = cfg.MAX_ROUNDS;
|
|
3656
3762
|
var WELL_KNOWN_URLS = cfg.WELL_KNOWN_URLS;
|
|
@@ -3781,8 +3887,10 @@ function boot(cfg) {
|
|
|
3781
3887
|
model: MODEL
|
|
3782
3888
|
});
|
|
3783
3889
|
}
|
|
3890
|
+
var pageCardEl = document.getElementById(PAGE_CARD_ID);
|
|
3891
|
+
var pageCard = readStaticPrimitivesCard(pageCardEl ? pageCardEl.textContent : null);
|
|
3784
3892
|
var actionsSchemaEl = document.getElementById(ACTIONS_SCHEMA_ID);
|
|
3785
|
-
var localTools = actionsSchemaEl ? JSON.parse(actionsSchemaEl.textContent) : [];
|
|
3893
|
+
var localTools = pageCard.tools.length ? pageCard.tools : actionsSchemaEl ? JSON.parse(actionsSchemaEl.textContent) : [];
|
|
3786
3894
|
var remoteToolsEl = document.getElementById(REMOTE_TOOLS_SCHEMA_ID);
|
|
3787
3895
|
var baselineRemoteTools = remoteToolsEl ? JSON.parse(remoteToolsEl.textContent) : [];
|
|
3788
3896
|
var remoteTools = baselineRemoteTools.slice();
|
|
@@ -4005,11 +4113,22 @@ function boot(cfg) {
|
|
|
4005
4113
|
}
|
|
4006
4114
|
}
|
|
4007
4115
|
var hostWideTools = [];
|
|
4008
|
-
var hostWidePrompts =
|
|
4116
|
+
var hostWidePrompts = pageCard.prompts.map(function(p) {
|
|
4117
|
+
return Object.assign({}, p, { source: "page" });
|
|
4118
|
+
});
|
|
4009
4119
|
var resourceContext = null;
|
|
4010
4120
|
var resourcePlan = null;
|
|
4011
4121
|
var RESOURCE_BUDGET_BYTES = 8e3;
|
|
4122
|
+
var pageResourcePlans = planPageResources({
|
|
4123
|
+
entries: pageCard.resources,
|
|
4124
|
+
budgetBytes: RESOURCE_BUDGET_BYTES,
|
|
4125
|
+
onSkip: function(plan) {
|
|
4126
|
+
console.info("[llm_meta_widget] not attaching " + plan.uri + " (" + plan.reason + ", size=" + plan.size + ")");
|
|
4127
|
+
}
|
|
4128
|
+
});
|
|
4129
|
+
var pageResourceCache = {};
|
|
4012
4130
|
restoreConversation();
|
|
4131
|
+
var promptsEl = root.querySelector(".lmw-prompts");
|
|
4013
4132
|
var wellKnownReady = (async function() {
|
|
4014
4133
|
var urls = WELL_KNOWN_URLS === null ? [window.location.origin + "/.well-known/mcp.json"] : WELL_KNOWN_URLS;
|
|
4015
4134
|
for (var i = 0; i < urls.length; i++) {
|
|
@@ -4031,7 +4150,7 @@ function boot(cfg) {
|
|
|
4031
4150
|
list: listMcpResources,
|
|
4032
4151
|
read: readMcpResource,
|
|
4033
4152
|
onSkip: function(plan) {
|
|
4034
|
-
console.info("[llm_meta_widget] not attaching " + plan.uri + " (" + plan.reason + ",
|
|
4153
|
+
console.info("[llm_meta_widget] not attaching " + plan.uri + " (" + plan.reason + ", size=" + plan.size + ")");
|
|
4035
4154
|
}
|
|
4036
4155
|
});
|
|
4037
4156
|
if (loaded && loaded.plan && loaded.plan.fetch) {
|
|
@@ -4043,7 +4162,6 @@ function boot(cfg) {
|
|
|
4043
4162
|
renderPromptButtons();
|
|
4044
4163
|
renderWelcome();
|
|
4045
4164
|
})();
|
|
4046
|
-
var promptsEl = root.querySelector(".lmw-prompts");
|
|
4047
4165
|
function renderPromptButtons() {
|
|
4048
4166
|
if (!promptsEl) return;
|
|
4049
4167
|
promptsEl.textContent = "";
|
|
@@ -4140,7 +4258,7 @@ function boot(cfg) {
|
|
|
4140
4258
|
}
|
|
4141
4259
|
button.disabled = true;
|
|
4142
4260
|
try {
|
|
4143
|
-
var result = await getMcpPrompt({ endpoint: prompt.endpoint, name: prompt.name, args });
|
|
4261
|
+
var result = prompt.source === "page" ? await getPagePrompt({ name: prompt.name, args, prompts: window[PROMPTS_GLOBAL] }) : await getMcpPrompt({ endpoint: prompt.endpoint, name: prompt.name, args });
|
|
4144
4262
|
var text = promptMessagesToText(result);
|
|
4145
4263
|
if (!text) {
|
|
4146
4264
|
appendTurn("error", "The server returned an empty prompt.");
|
|
@@ -4354,7 +4472,14 @@ function boot(cfg) {
|
|
|
4354
4472
|
budgetBytes: RESOURCE_BUDGET_BYTES
|
|
4355
4473
|
});
|
|
4356
4474
|
resourceContext = turn.cached;
|
|
4357
|
-
|
|
4475
|
+
var page = await pageResourceLinesForTurn({
|
|
4476
|
+
plans: pageResourcePlans,
|
|
4477
|
+
resources: window[RESOURCES_GLOBAL],
|
|
4478
|
+
budgetBytes: RESOURCE_BUDGET_BYTES,
|
|
4479
|
+
cache: pageResourceCache
|
|
4480
|
+
});
|
|
4481
|
+
pageResourceCache = page.cache;
|
|
4482
|
+
return turn.lines.concat(page.lines);
|
|
4358
4483
|
}
|
|
4359
4484
|
var currentAbort = null;
|
|
4360
4485
|
clearBtn.addEventListener("click", function() {
|
|
@@ -730,6 +730,28 @@ export async function readMcpResource({ endpoint, uri, signal }) {
|
|
|
730
730
|
// The spec allows `content` to be an object or an array of content blocks;
|
|
731
731
|
// anything non-text is dropped, which is worth knowing — a prompt carrying an
|
|
732
732
|
// image would silently lose it on this path.
|
|
733
|
+
// A prompt the PAGE declared, resolved against its handler map. This is
|
|
734
|
+
// prompts/get for a surface with no transport: the card carries the metadata
|
|
735
|
+
// (name, description, arguments — what prompts/list returns) and the handler
|
|
736
|
+
// returns the filled messages, exactly as a server's prompts/get would.
|
|
737
|
+
//
|
|
738
|
+
// Keyed by `name` because that is what prompts/get takes. Accepts a plain
|
|
739
|
+
// string as well as a messages array: a page prompt is usually one block of
|
|
740
|
+
// text, and making every host write the envelope by hand would be a tax with
|
|
741
|
+
// no benefit — promptMessagesToText flattens both to the same thing.
|
|
742
|
+
export async function getPagePrompt({ name, args, prompts }) {
|
|
743
|
+
const handler = prompts && prompts[name]
|
|
744
|
+
if (typeof handler !== "function") {
|
|
745
|
+
throw new Error('the page declares prompt "' + name + '" but no handler answers to that name')
|
|
746
|
+
}
|
|
747
|
+
|
|
748
|
+
const result = await handler(args || {})
|
|
749
|
+
if (typeof result === "string") {
|
|
750
|
+
return { messages: [ { role: "user", content: { type: "text", text: result } } ] }
|
|
751
|
+
}
|
|
752
|
+
return result || { messages: [] }
|
|
753
|
+
}
|
|
754
|
+
|
|
733
755
|
export function promptMessagesToText(result) {
|
|
734
756
|
return (result?.messages || [])
|
|
735
757
|
.map((m) => {
|
|
@@ -819,8 +841,13 @@ function parseSseFrame(raw) {
|
|
|
819
841
|
// follow-on). Three optional fields a server may declare on a resources/list
|
|
820
842
|
// entry:
|
|
821
843
|
//
|
|
822
|
-
//
|
|
844
|
+
// size - byte count of the payload resources/read would return, so a
|
|
823
845
|
// client can decide whether to attach it BEFORE fetching it.
|
|
846
|
+
// This is the BASE Resource field, not an extension field: the
|
|
847
|
+
// extension only strengthens it to "MUST be exact when present".
|
|
848
|
+
// Read from `_meta.sizeBytes` too, which is where the earlier
|
|
849
|
+
// draft put it — servers deployed against that draft (PD among
|
|
850
|
+
// them) are still emitting it.
|
|
824
851
|
// volatility - "stable" (content fixed) or "volatile" (varies between
|
|
825
852
|
// turns, so a client re-reads it each turn).
|
|
826
853
|
// autoAttach - may a client attach this without the user asking for it?
|
|
@@ -835,29 +862,104 @@ function parseSseFrame(raw) {
|
|
|
835
862
|
// the first turn only.
|
|
836
863
|
export const STATIC_PRIMITIVES_META = "io.modelcontextprotocol/static-primitives"
|
|
837
864
|
|
|
865
|
+
// ---- page card --------------------------------------------------------
|
|
866
|
+
//
|
|
867
|
+
// The same `io.modelcontextprotocol/static-primitives` envelope a server puts
|
|
868
|
+
// in its Server Card, delivered in the DOCUMENT instead: one <script> of JSON
|
|
869
|
+
// declaring the page's own tools, prompts and resources. SEP-2575 already says
|
|
870
|
+
// the Server Card and server/discover are two deliveries of one content; a page
|
|
871
|
+
// is a third, with no transport at all.
|
|
872
|
+
//
|
|
873
|
+
// Parsing only. What a declaration MEANS is resolved elsewhere: tool handlers
|
|
874
|
+
// live in window.aiActions keyed by `name`, and reads will be keyed by the
|
|
875
|
+
// protocol's own identifier — `name` for prompts, `uri` for resources, because
|
|
876
|
+
// that is what prompts/get and resources/read take.
|
|
877
|
+
//
|
|
878
|
+
// Malformed input must never take the page down: a card is host-authored and a
|
|
879
|
+
// typo in it should cost that one entry, not the assistant. Everything
|
|
880
|
+
// unparseable returns empty arrays and says so on the console, which is how
|
|
881
|
+
// aiState already treats a reader missing its description.
|
|
882
|
+
export function readStaticPrimitivesCard(text, warn = console.warn) {
|
|
883
|
+
const empty = { tools: [], prompts: [], resources: [] }
|
|
884
|
+
if (typeof text !== "string" || text.trim() === "") return empty
|
|
885
|
+
|
|
886
|
+
let doc
|
|
887
|
+
try {
|
|
888
|
+
doc = JSON.parse(text)
|
|
889
|
+
} catch (e) {
|
|
890
|
+
warn("[llm-meta-widget] page card is not valid JSON; ignoring it:", e.message)
|
|
891
|
+
return empty
|
|
892
|
+
}
|
|
893
|
+
|
|
894
|
+
const ext = (doc && doc._meta && doc._meta[STATIC_PRIMITIVES_META]) || null
|
|
895
|
+
if (!ext) {
|
|
896
|
+
warn(`[llm-meta-widget] page card has no _meta["${STATIC_PRIMITIVES_META}"]; ignoring it`)
|
|
897
|
+
return empty
|
|
898
|
+
}
|
|
899
|
+
|
|
900
|
+
const named = (arr, kind) => (Array.isArray(arr) ? arr : []).filter((e) => {
|
|
901
|
+
const ok = e && typeof e.name === "string" && e.name.trim() !== ""
|
|
902
|
+
if (!ok) warn(`[llm-meta-widget] page card: dropping a ${kind} with no name`)
|
|
903
|
+
return ok
|
|
904
|
+
})
|
|
905
|
+
|
|
906
|
+
return {
|
|
907
|
+
tools: named(ext.tools, "tool"),
|
|
908
|
+
prompts: named(ext.prompts, "prompt"),
|
|
909
|
+
// Resources are addressed by uri, not name — resources/read takes the uri.
|
|
910
|
+
resources: (Array.isArray(ext.resources) ? ext.resources : []).filter((e) => {
|
|
911
|
+
const ok = e && typeof e.uri === "string" && e.uri.trim() !== ""
|
|
912
|
+
if (!ok) warn("[llm-meta-widget] page card: dropping a resource with no uri")
|
|
913
|
+
return ok
|
|
914
|
+
})
|
|
915
|
+
}
|
|
916
|
+
}
|
|
917
|
+
|
|
838
918
|
const VOLATILITIES = [ "stable", "volatile" ]
|
|
839
919
|
|
|
920
|
+
const usableSize = (n) => (typeof n === "number" && isFinite(n) ? n : null)
|
|
921
|
+
const pickDeclared = (own, fromMeta) => (own === undefined ? fromMeta : own)
|
|
922
|
+
|
|
840
923
|
export function resourceHints(entry) {
|
|
841
924
|
const meta = (entry && entry._meta && entry._meta[STATIC_PRIMITIVES_META]) || {}
|
|
842
925
|
return {
|
|
843
926
|
// A non-numeric or absent size means "unknown", never 0 — 0 would read
|
|
844
927
|
// as a free resource and sail through every budget check.
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
928
|
+
// Base `size` wins; `_meta.sizeBytes` is the earlier draft's spelling and
|
|
929
|
+
// is accepted so a new client keeps its budget gate against an old server.
|
|
930
|
+
size: usableSize(entry && entry.size) ?? usableSize(meta.sizeBytes),
|
|
931
|
+
// Deprecated alias, kept for one release so renaming the field is not a
|
|
932
|
+
// breaking change: orchestrator.js is a published entry point, so anything
|
|
933
|
+
// it returns is public API. Dropped at 0.9.0 together with the
|
|
934
|
+
// `_meta.sizeBytes` fallback above — one deprecation, one bump.
|
|
935
|
+
get sizeBytes() { return this.size },
|
|
936
|
+
// Same rule as `size`: the entry's own field wins, `_meta` is the fallback.
|
|
937
|
+
// SEP-0000 defines these ON the Resource object; a server puts them in
|
|
938
|
+
// `_meta` only because the extension is not merged yet. A PAGE card's
|
|
939
|
+
// entries already sit inside the extension block, so for them the fields
|
|
940
|
+
// are simply top-level — reading both is what lets one planner serve both.
|
|
941
|
+
volatility: VOLATILITIES.indexOf(pickDeclared(entry && entry.volatility, meta.volatility)) === -1
|
|
942
|
+
? "stable"
|
|
943
|
+
: pickDeclared(entry && entry.volatility, meta.volatility),
|
|
944
|
+
autoAttach: typeof pickDeclared(entry && entry.autoAttach, meta.autoAttach) === "boolean"
|
|
945
|
+
? pickDeclared(entry && entry.autoAttach, meta.autoAttach)
|
|
946
|
+
: true
|
|
848
947
|
}
|
|
849
948
|
}
|
|
850
949
|
|
|
851
950
|
// The pre-flight decision, made from the resources/list entry alone.
|
|
852
951
|
// `fetch: false` means the bytes never cross the wire at all.
|
|
853
952
|
export function planResourceAttachment(entry, budgetBytes) {
|
|
854
|
-
const {
|
|
855
|
-
|
|
953
|
+
const { size, volatility, autoAttach } = resourceHints(entry)
|
|
954
|
+
// `sizeBytes` is the deprecated alias again: this function is exported too,
|
|
955
|
+
// and it returned that name before the field was aligned with the base
|
|
956
|
+
// Resource interface. Dropped at 0.9.0 with the rest.
|
|
957
|
+
const base = { size, sizeBytes: size, volatility, uri: entry && entry.uri }
|
|
856
958
|
|
|
857
959
|
if (!autoAttach) {
|
|
858
960
|
return { ...base, fetch: false, autoAttach: false, reason: "not-auto-attach" }
|
|
859
961
|
}
|
|
860
|
-
if (
|
|
962
|
+
if (size !== null && size > budgetBytes) {
|
|
861
963
|
// The client's budget overrides the server's willingness — which is why
|
|
862
964
|
// autoAttach has to be a boolean here rather than a restatement of a hint.
|
|
863
965
|
return { ...base, fetch: false, autoAttach: false, reason: "over-budget" }
|
|
@@ -868,7 +970,7 @@ export function planResourceAttachment(entry, budgetBytes) {
|
|
|
868
970
|
autoAttach: true,
|
|
869
971
|
// Unknown size still gets fetched, then trimmed — the trim is the
|
|
870
972
|
// safety net for servers that do not declare a size.
|
|
871
|
-
reason:
|
|
973
|
+
reason: size === null ? "size-unknown" : "within-budget"
|
|
872
974
|
}
|
|
873
975
|
}
|
|
874
976
|
|
|
@@ -903,6 +1005,65 @@ async function readResourceContext({ endpoint, uri, name, read, budgetBytes }) {
|
|
|
903
1005
|
}
|
|
904
1006
|
}
|
|
905
1007
|
|
|
1008
|
+
// ---- page resources ----------------------------------------------------
|
|
1009
|
+
//
|
|
1010
|
+
// resources/read for a surface with no transport. The card lists them, a
|
|
1011
|
+
// handler map keyed by URI reads them — URI because that is what
|
|
1012
|
+
// resources/read takes, and because the spec defines it as the unique
|
|
1013
|
+
// identifier while `name` is only required, never unique.
|
|
1014
|
+
//
|
|
1015
|
+
// Unlike a server endpoint, a page declares SEVERAL: PubDictionaries has six
|
|
1016
|
+
// readers, each with its own description. So these are planned and rendered as
|
|
1017
|
+
// a list, not as the single reference resource loadHostResource assumes.
|
|
1018
|
+
export function planPageResources({ entries, budgetBytes, onSkip }) {
|
|
1019
|
+
return (entries || []).map((entry) => {
|
|
1020
|
+
const plan = planResourceAttachment(entry, budgetBytes)
|
|
1021
|
+
plan.name = entry.name || entry.uri
|
|
1022
|
+
plan.description = entry.description || ""
|
|
1023
|
+
if (!plan.fetch && onSkip) onSkip(plan)
|
|
1024
|
+
return plan
|
|
1025
|
+
}).filter((plan) => plan.fetch)
|
|
1026
|
+
}
|
|
1027
|
+
|
|
1028
|
+
// One compact line per resource — the same shape the page-state annex uses,
|
|
1029
|
+
// NOT the block form a hub resource gets. Six block-form resources would cost
|
|
1030
|
+
// three lines each and repeat a header the model does not need; how to render
|
|
1031
|
+
// attached context is the client's decision, which the spec leaves open.
|
|
1032
|
+
export function pageResourceLine(plan, text) {
|
|
1033
|
+
return "- " + plan.name + (plan.description ? " (" + plan.description + ")" : "") + ": " + text
|
|
1034
|
+
}
|
|
1035
|
+
|
|
1036
|
+
export async function pageResourceLinesForTurn({ plans, resources, budgetBytes, cache = {} }) {
|
|
1037
|
+
const lines = []
|
|
1038
|
+
for (const plan of plans || []) {
|
|
1039
|
+
let text = plan.volatility === "volatile" ? null : cache[plan.uri]
|
|
1040
|
+
if (text === null || text === undefined) {
|
|
1041
|
+
text = await readPageResource({ uri: plan.uri, resources, budgetBytes })
|
|
1042
|
+
if (plan.volatility !== "volatile" && text !== null) cache[plan.uri] = text
|
|
1043
|
+
}
|
|
1044
|
+
if (text !== null) lines.push(pageResourceLine(plan, text))
|
|
1045
|
+
}
|
|
1046
|
+
return { lines, cache }
|
|
1047
|
+
}
|
|
1048
|
+
|
|
1049
|
+
async function readPageResource({ uri, resources, budgetBytes }) {
|
|
1050
|
+
const handler = resources && resources[uri]
|
|
1051
|
+
if (typeof handler !== "function") {
|
|
1052
|
+
console.warn('[llm-meta-widget] the page declares resource "' + uri + '" but no handler answers to that uri')
|
|
1053
|
+
return null
|
|
1054
|
+
}
|
|
1055
|
+
try {
|
|
1056
|
+
const value = await handler()
|
|
1057
|
+
const text = typeof value === "string" ? value : JSON.stringify(value)
|
|
1058
|
+
return trimResourceText(text === undefined ? "" : text, budgetBytes)
|
|
1059
|
+
} catch (e) {
|
|
1060
|
+
// Optional context — one broken reader must not cost the turn, exactly as
|
|
1061
|
+
// a failed hub read does not.
|
|
1062
|
+
console.warn('[llm-meta-widget] reading "' + uri + '" failed:', e && e.message)
|
|
1063
|
+
return null
|
|
1064
|
+
}
|
|
1065
|
+
}
|
|
1066
|
+
|
|
906
1067
|
// The discovery sequence for one endpoint's reference resource, kept here
|
|
907
1068
|
// rather than in the panel so the decision AND the call site are testable
|
|
908
1069
|
// together: a gate nothing consults is exactly the bug this shape prevents.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@aibranch/llm-meta-widget",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.9.0",
|
|
4
4
|
"//version": "Must stay in step with lib/llm_meta_widget/version.rb. Two independently versioned copies of the same client is the drift docs/custom-element-distribution.md warns about: bump both, release both.",
|
|
5
5
|
"description": "Embeddable browser chat widget: a custom element that discovers tools at page, origin and hub scope and dispatches each tool call to the right one. Two lines of HTML on any host, Rails or not.",
|
|
6
6
|
"keywords": [
|