llm_meta_widget 0.2.0 → 0.3.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: f32625e105ab751f473e79517d113c8b84465e676cddaab0f3db861c8f3e563e
4
- data.tar.gz: 249d09f198b93e1b8382b2f5ef719736b260fc4c0c2f53d77e68c203b8e236b2
3
+ metadata.gz: 6d7a58b9b553be09b66ca6e9aa01f8264a280bb725f80820ec3de506573701ae
4
+ data.tar.gz: 7b171f0c86c5f45a6250975ab02da586d620602d8fc1bea95261dd963d17e67a
5
5
  SHA512:
6
- metadata.gz: 841f81296bed31dbe0ebd7c67253b08990be954a0e5b6939f0563c3d97cb525b8826654149350dbae4afd74ce5b61750058f289c9df25ed4fca5b577783db72b
7
- data.tar.gz: ced44aaf75dc424014f878360a4f769247cda1856abb7ffde7e3d66e829e8ae953e1c5b0ba21aea6521abab19b10ac821631e490d9ca47cba7876ea41b5ef8d1
6
+ metadata.gz: e90d55856a37217816264730d1a65fd32af2ad4783ad0c1a09ed298fb5dfe35a618a1ca149bf77265aab346dba4437b960a98399e91b3b15695aa601ed9d2025
7
+ data.tar.gz: 042b6872987730e708758fee7c6043ac27b0a83b27935e33f0751a910307cd901e746f28a2557dc54dfc32f63d5a4039f43cefcbc1952d93613cc59568e4c44a
@@ -780,38 +780,50 @@ function parseSseFrame(raw) {
780
780
  // ---- static-primitives extension ---------------------------------------
781
781
  //
782
782
  // Prototype of `io.modelcontextprotocol/static-primitives` (the SEP-2127
783
- // follow-on): two optional fields a server may declare on a resources/list
784
- // entry. `sizeBytes` is the byte length of the payload resources/read would
785
- // return, so a client can decide whether to attach it BEFORE fetching it.
786
- // `attachmentHint` says how often attaching is worth it.
783
+ // follow-on). Three optional fields a server may declare on a resources/list
784
+ // entry:
787
785
  //
788
- // Both are optional, and a server that declares neither must behave exactly
789
- // as it did before the extension existed: fetch, trim to budget, attach once.
786
+ // sizeBytes - byte length of the payload resources/read would return, so a
787
+ // client can decide whether to attach it BEFORE fetching it.
788
+ // volatility - "stable" (content fixed) or "volatile" (varies between
789
+ // turns, so a client re-reads it each turn).
790
+ // autoAttach - may a client attach this without the user asking for it?
791
+ //
792
+ // volatility and autoAttach are separate on purpose. Whether content changes
793
+ // says nothing about whether it may be attached unasked, and the client needs
794
+ // autoAttach as a boolean anyway, because its own byte budget can withhold a
795
+ // resource the server was happy to hand over.
796
+ //
797
+ // All three are optional, and a server declaring none must behave exactly as
798
+ // it did before the extension existed: fetch once, trim to budget, attach on
799
+ // the first turn only.
790
800
  export const STATIC_PRIMITIVES_META = "io.modelcontextprotocol/static-primitives"
791
801
 
792
- const ATTACHMENT_HINTS = [ "once", "each-turn", "on-demand" ]
802
+ const VOLATILITIES = [ "stable", "volatile" ]
793
803
 
794
804
  export function resourceHints(entry) {
795
805
  const meta = (entry && entry._meta && entry._meta[STATIC_PRIMITIVES_META]) || {}
796
- const hint = meta.attachmentHint
797
806
  return {
798
807
  // A non-numeric or absent size means "unknown", never 0 — 0 would read
799
808
  // as a free resource and sail through every budget check.
800
809
  sizeBytes: typeof meta.sizeBytes === "number" && isFinite(meta.sizeBytes) ? meta.sizeBytes : null,
801
- attachmentHint: ATTACHMENT_HINTS.indexOf(hint) === -1 ? "once" : hint
810
+ volatility: VOLATILITIES.indexOf(meta.volatility) === -1 ? "stable" : meta.volatility,
811
+ autoAttach: typeof meta.autoAttach === "boolean" ? meta.autoAttach : true
802
812
  }
803
813
  }
804
814
 
805
815
  // The pre-flight decision, made from the resources/list entry alone.
806
816
  // `fetch: false` means the bytes never cross the wire at all.
807
817
  export function planResourceAttachment(entry, budgetBytes) {
808
- const { sizeBytes, attachmentHint } = resourceHints(entry)
809
- const base = { sizeBytes, attachmentHint, uri: entry && entry.uri }
818
+ const { sizeBytes, volatility, autoAttach } = resourceHints(entry)
819
+ const base = { sizeBytes, volatility, uri: entry && entry.uri }
810
820
 
811
- if (attachmentHint === "on-demand") {
812
- return { ...base, fetch: false, autoAttach: false, reason: "on-demand" }
821
+ if (!autoAttach) {
822
+ return { ...base, fetch: false, autoAttach: false, reason: "not-auto-attach" }
813
823
  }
814
824
  if (sizeBytes !== null && sizeBytes > budgetBytes) {
825
+ // The client's budget overrides the server's willingness — which is why
826
+ // autoAttach has to be a boolean here rather than a restatement of a hint.
815
827
  return { ...base, fetch: false, autoAttach: false, reason: "over-budget" }
816
828
  }
817
829
  return {
@@ -824,21 +836,19 @@ export function planResourceAttachment(entry, budgetBytes) {
824
836
  }
825
837
  }
826
838
 
827
- // Per-turn gate. Replaces a single `sent` boolean, which silently assumed
828
- // every resource was "once" and would have re-sent nothing for a resource
829
- // whose content actually varies between turns.
839
+ // Per-turn gate. A stable resource is attached once and re-used; a volatile
840
+ // one is owed a fresh copy every turn.
830
841
  export function createResourceAttacher(plan) {
831
842
  let attachedOnce = false
832
843
  return {
833
- // Returns whether to attach on THIS turn, and records the answer.
834
844
  take() {
835
845
  if (!plan || !plan.autoAttach) return false
836
- if (plan.attachmentHint === "each-turn") return true
846
+ if (plan.volatility === "volatile") return true
837
847
  if (attachedOnce) return false
838
848
  attachedOnce = true
839
849
  return true
840
850
  },
841
- get attachmentHint() { return plan ? plan.attachmentHint : null }
851
+ get volatility() { return plan ? plan.volatility : null }
842
852
  }
843
853
  }
844
854
 
@@ -858,38 +868,69 @@ export function trimResourceText(text, maxBytes) {
858
868
  return text.slice(0, maxBytes) + "\n…truncated"
859
869
  }
860
870
 
861
- // The whole discovery sequence for one endpoint's reference resource, kept
862
- // here rather than in the panel so the decision AND the call site are
863
- // testable together: a gate that nothing consults is the failure mode this
864
- // replaces. `list` and `read` are injected so a test can assert that an
865
- // over-budget resource is never read.
871
+ // Read one resource and shape it for the system prompt. The trim is applied
872
+ // on every read, not just the first: a declared size can go stale, and a
873
+ // volatile resource is a fresh gamble each turn.
874
+ async function readResourceContext({ endpoint, uri, name, read, budgetBytes }) {
875
+ try {
876
+ const result = await read({ endpoint, uri })
877
+ const entry = ((result && result.contents) || [])[0]
878
+ if (!entry || !entry.text) return null
879
+ return { uri, name: name || uri, text: trimResourceText(entry.text, budgetBytes) }
880
+ } catch (e) {
881
+ // Optional context — a failed read must never block the widget.
882
+ return null
883
+ }
884
+ }
885
+
886
+ // The discovery sequence for one endpoint's reference resource, kept here
887
+ // rather than in the panel so the decision AND the call site are testable
888
+ // together: a gate nothing consults is exactly the bug this shape prevents.
889
+ //
890
+ // A volatile resource is NOT read here. Its content is only meaningful for
891
+ // the turn it is attached to, so reading it at boot would buy a copy that is
892
+ // already suspect by the time anyone sends a message.
866
893
  export async function loadHostResource({ endpoint, budgetBytes, list, read, onSkip }) {
867
894
  const resources = await list({ endpoint })
868
895
  const candidate = (resources || []).filter((r) => (r.mimeType || "") === "application/json")[0]
869
896
  if (!candidate) return null
870
897
 
871
898
  const plan = planResourceAttachment(candidate, budgetBytes)
899
+ plan.name = candidate.name || candidate.uri
900
+ // Carried so a volatile re-read knows where to go: discovery happens once
901
+ // at boot, but the fetch it authorises happens on every later turn.
902
+ plan.endpoint = endpoint
872
903
  if (!plan.fetch) {
873
904
  if (onSkip) onSkip(plan)
874
905
  return { plan, context: null }
875
906
  }
907
+ if (plan.volatility === "volatile") return { plan, context: null }
876
908
 
877
- try {
878
- const result = await read({ endpoint, uri: candidate.uri })
879
- const entry = ((result && result.contents) || [])[0]
880
- if (!entry || !entry.text) return { plan, context: null }
881
- return {
882
- plan,
883
- context: {
884
- uri: candidate.uri,
885
- name: candidate.name || candidate.uri,
886
- text: trimResourceText(entry.text, budgetBytes)
887
- }
888
- }
889
- } catch (e) {
890
- // Optional context a failed read must never block the widget.
891
- return { plan, context: null }
909
+ const context = await readResourceContext({
910
+ endpoint, uri: candidate.uri, name: plan.name, read, budgetBytes
911
+ })
912
+ return { plan, context }
913
+ }
914
+
915
+ export function resourceContextLines(context) {
916
+ if (!context) return []
917
+ return [ "", "Reference data — " + context.name + " (" + context.uri + "):", context.text ]
918
+ }
919
+
920
+ // What to attach on THIS turn. Asking consumes the turn, so the decision and
921
+ // the fetch live together: a stable resource re-uses the copy read at boot,
922
+ // a volatile one is read again right now.
923
+ export async function resourceLinesForTurn({ plan, attacher, cached, endpoint, read, budgetBytes }) {
924
+ if (!attacher || !attacher.take()) return { lines: [], cached }
925
+
926
+ if (plan && plan.volatility === "volatile") {
927
+ const fresh = await readResourceContext({
928
+ endpoint, uri: plan.uri, name: plan.name, read, budgetBytes
929
+ })
930
+ return fresh ? { lines: resourceContextLines(fresh), cached: fresh } : { lines: [], cached }
892
931
  }
932
+
933
+ return { lines: resourceContextLines(cached), cached }
893
934
  }
894
935
 
895
936
  // ---- prompt templates ---------------------------------------------------
@@ -937,9 +978,3 @@ export function promptButtonProps(prompt) {
937
978
  }
938
979
  }
939
980
 
940
- // The per-turn attach decision AND its formatting, so the two cannot drift
941
- // apart: asking whether to attach is what consumes the turn.
942
- export function nextResourceContextLines(context, attacher) {
943
- if (!context || !attacher || !attacher.take()) return []
944
- return [ "", "Reference data — " + context.name + " (" + context.uri + "):", context.text ]
945
- }
@@ -398,8 +398,8 @@
398
398
  <script type="module">
399
399
  import { runChatLoop, fetchMcpManifest, listMcpPrompts, getMcpPrompt,
400
400
  listMcpResources, readMcpResource, promptMessagesToText,
401
- loadHostResource, createResourceAttacher, resolvePromptArguments,
402
- promptButtonProps, nextResourceContextLines } from "<%= orchestrator_path %>";
401
+ loadHostResource, createResourceAttacher, resourceLinesForTurn, resolvePromptArguments,
402
+ promptButtonProps } from "<%= orchestrator_path %>";
403
403
  import { marked } from "/llm_meta_widget_assets/marked.esm.js";
404
404
 
405
405
  // Standard prose settings — GFM (tables, autolinks, strikethrough),
@@ -789,7 +789,7 @@
789
789
  var prompts = await listMcpPrompts({ endpoint: endpoint });
790
790
  hostWidePrompts = hostWidePrompts.concat(prompts);
791
791
 
792
- if (resourceContext === null) {
792
+ if (resourcePlan === null) {
793
793
  // Listing, size gate and read all live in the orchestrator, where
794
794
  // they are tested together — a gate nothing consults is exactly
795
795
  // the bug this shape prevents.
@@ -804,7 +804,10 @@
804
804
  " (" + plan.reason + ", sizeBytes=" + plan.sizeBytes + ")");
805
805
  }
806
806
  });
807
- if (loaded && loaded.context) {
807
+ // A volatile resource comes back with no context — it is read
808
+ // per turn instead — so the plan, not the payload, is what says
809
+ // whether this endpoint offered anything worth attaching.
810
+ if (loaded && loaded.plan && loaded.plan.fetch) {
808
811
  resourceContext = loaded.context;
809
812
  resourcePlan = loaded.plan;
810
813
  resourceAttacher = createResourceAttacher(loaded.plan);
@@ -960,7 +963,7 @@
960
963
  return out;
961
964
  }
962
965
 
963
- function currentSystemPrompt() {
966
+ function currentSystemPrompt(resourceLines) {
964
967
  return [
965
968
  "You are integrated into a web page as an AI assistant. You have tools available to change page state or fetch information.",
966
969
  "",
@@ -972,16 +975,27 @@
972
975
  "",
973
976
  "Current page state:",
974
977
  JSON.stringify(currentPageState(), null, 2)
975
- ].concat(resourceContextLines()).join("\n");
978
+ ].concat(resourceLines || []).join("\n");
976
979
  }
977
980
 
978
- // How often the resource is attached is the SERVER's call, via the
979
- // extension's attachmentHint: 'once' for static reference data (the
980
- // default, and what a hint-unaware server gets), 'each-turn' for content
981
- // that varies. The attacher records each turn's answer, replacing a
982
- // client-side boolean that assumed every resource was static.
983
- function resourceContextLines() {
984
- return nextResourceContextLines(resourceContext, resourceAttacher);
981
+ // Whether to attach, and how often, is the SERVER's call via the
982
+ // extension's autoAttach and volatility fields, which default to
983
+ // attach-once for a server that declares neither.
984
+ //
985
+ // Asking consumes the turn, and a volatile resource is re-read here rather
986
+ // than re-using the boot-time copy — so this runs once per send, before
987
+ // the system prompt is built.
988
+ async function resourceLinesForThisTurn() {
989
+ var turn = await resourceLinesForTurn({
990
+ plan: resourcePlan,
991
+ attacher: resourceAttacher,
992
+ cached: resourceContext,
993
+ endpoint: resourcePlan && resourcePlan.endpoint,
994
+ read: readMcpResource,
995
+ budgetBytes: RESOURCE_BUDGET_BYTES
996
+ });
997
+ resourceContext = turn.cached;
998
+ return turn.lines;
985
999
  }
986
1000
 
987
1001
  // Per-turn AbortController — lets the Clear button (or a new submit)
@@ -1028,7 +1042,8 @@
1028
1042
  // it as sent.
1029
1043
  await wellKnownReady;
1030
1044
 
1031
- var messages = [{ role: "system", content: currentSystemPrompt() }]
1045
+ var resourceLines = await resourceLinesForThisTurn();
1046
+ var messages = [{ role: "system", content: currentSystemPrompt(resourceLines) }]
1032
1047
  .concat(conversation)
1033
1048
  .concat([{ role: "user", content: userText }]);
1034
1049
 
@@ -1,3 +1,3 @@
1
1
  module LlmMetaWidget
2
- VERSION = "0.2.0"
2
+ VERSION = "0.3.0"
3
3
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: llm_meta_widget
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.0
4
+ version: 0.3.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - jdkim