@guuey/agent-client 0.2.3 → 0.3.1

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.
package/dist/history.d.ts CHANGED
@@ -10,7 +10,7 @@
10
10
  * `credentials: "include"`). Consumed by {@link createWebAdapters}; Portal
11
11
  * has its own copy today and can migrate onto this later.
12
12
  */
13
- import type { JsonValue } from "@silverprotocol/core";
13
+ import type { AgMessage, JsonValue } from "@silverprotocol/core";
14
14
  import type { AgentMessage, HistoryCard, HistoryLoadResult } from "./types";
15
15
  /** One row of `GET /v1/threads/:id/messages`. */
16
16
  export interface ThreadHistoryRow {
@@ -71,4 +71,17 @@ export interface ThreadHistoryFetchOptions {
71
71
  * the chat empty.
72
72
  */
73
73
  export declare function fetchThreadHistory({ baseUrl, threadId, requestInit, includeCards, fetchImpl, }: ThreadHistoryFetchOptions): Promise<HistoryLoadResult>;
74
+ /**
75
+ * The tool name for a `tool-result` block, read off its paired `tool-call`
76
+ * block in the same message (the reducer keeps both in one message's content).
77
+ * Falls back to `"tool"` when the pair is missing.
78
+ */
79
+ export declare function toolNameFor(message: AgMessage, toolCallId: string): string;
80
+ /**
81
+ * Persisted cards, ascending by transcript `seq` (stable; input untouched).
82
+ * These are PRIOR-turn cards — they always precede the live fold, so a
83
+ * block-preserving renderer surfaces them first (e.g. under an "Earlier in
84
+ * this conversation" divider).
85
+ */
86
+ export declare function sortHistoryCards(cards: readonly HistoryCard[]): HistoryCard[];
74
87
  //# sourceMappingURL=history.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"history.d.ts","sourceRoot":"","sources":["../src/history.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AACH,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,sBAAsB,CAAC;AACtD,OAAO,KAAK,EAAE,YAAY,EAAE,WAAW,EAAE,iBAAiB,EAAE,MAAM,SAAS,CAAC;AAE5E,iDAAiD;AACjD,MAAM,WAAW,gBAAgB;IAC/B,GAAG,EAAE,MAAM,CAAC;IACZ,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,MAAM,CAAC;IACb,UAAU,EAAE,MAAM,CAAC;IACnB,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;IACpB;;;;OAIG;IACH,YAAY,CAAC,EAAE,SAAS,GAAG,IAAI,CAAC;CACjC;AAOD;;;;;;;;GAQG;AACH,qBAAa,wBAAyB,SAAQ,KAAK;gBACrC,OAAO,SAA6B;CAIjD;AAeD,qEAAqE;AACrE,wBAAgB,2BAA2B,CAAC,IAAI,EAAE,gBAAgB,EAAE,GAAG,YAAY,EAAE,CAOpF;AAED;;;;;GAKG;AACH,wBAAgB,wBAAwB,CAAC,IAAI,EAAE,gBAAgB,EAAE,GAAG,WAAW,EAAE,CAOhF;AAED,MAAM,WAAW,yBAAyB;IACxC,uDAAuD;IACvD,OAAO,EAAE,MAAM,CAAC;IAChB,QAAQ,EAAE,MAAM,CAAC;IACjB,2EAA2E;IAC3E,WAAW,CAAC,EAAE,WAAW,CAAC;IAC1B;;;;;OAKG;IACH,YAAY,CAAC,EAAE,OAAO,CAAC;IACvB,gEAAgE;IAChE,SAAS,CAAC,EAAE,OAAO,KAAK,CAAC;CAC1B;AAED;;;;;;GAMG;AACH,wBAAsB,kBAAkB,CAAC,EACvC,OAAO,EACP,QAAQ,EACR,WAAW,EACX,YAAoB,EACpB,SAAiB,GAClB,EAAE,yBAAyB,GAAG,OAAO,CAAC,iBAAiB,CAAC,CAuBxD"}
1
+ {"version":3,"file":"history.d.ts","sourceRoot":"","sources":["../src/history.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AACH,OAAO,KAAK,EAAE,SAAS,EAAE,SAAS,EAAE,MAAM,sBAAsB,CAAC;AACjE,OAAO,KAAK,EAAE,YAAY,EAAE,WAAW,EAAE,iBAAiB,EAAE,MAAM,SAAS,CAAC;AAE5E,iDAAiD;AACjD,MAAM,WAAW,gBAAgB;IAC/B,GAAG,EAAE,MAAM,CAAC;IACZ,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,MAAM,CAAC;IACb,UAAU,EAAE,MAAM,CAAC;IACnB,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;IACpB;;;;OAIG;IACH,YAAY,CAAC,EAAE,SAAS,GAAG,IAAI,CAAC;CACjC;AAOD;;;;;;;;GAQG;AACH,qBAAa,wBAAyB,SAAQ,KAAK;gBACrC,OAAO,SAA6B;CAIjD;AAeD,qEAAqE;AACrE,wBAAgB,2BAA2B,CAAC,IAAI,EAAE,gBAAgB,EAAE,GAAG,YAAY,EAAE,CAOpF;AAED;;;;;GAKG;AACH,wBAAgB,wBAAwB,CAAC,IAAI,EAAE,gBAAgB,EAAE,GAAG,WAAW,EAAE,CAOhF;AAED,MAAM,WAAW,yBAAyB;IACxC,uDAAuD;IACvD,OAAO,EAAE,MAAM,CAAC;IAChB,QAAQ,EAAE,MAAM,CAAC;IACjB,2EAA2E;IAC3E,WAAW,CAAC,EAAE,WAAW,CAAC;IAC1B;;;;;OAKG;IACH,YAAY,CAAC,EAAE,OAAO,CAAC;IACvB,gEAAgE;IAChE,SAAS,CAAC,EAAE,OAAO,KAAK,CAAC;CAC1B;AAED;;;;;;GAMG;AACH,wBAAsB,kBAAkB,CAAC,EACvC,OAAO,EACP,QAAQ,EACR,WAAW,EACX,YAAoB,EACpB,SAAiB,GAClB,EAAE,yBAAyB,GAAG,OAAO,CAAC,iBAAiB,CAAC,CAuBxD;AAED;;;;GAIG;AACH,wBAAgB,WAAW,CAAC,OAAO,EAAE,SAAS,EAAE,UAAU,EAAE,MAAM,GAAG,MAAM,CAK1E;AAED;;;;;GAKG;AACH,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,SAAS,WAAW,EAAE,GAAG,WAAW,EAAE,CAE7E"}
package/dist/history.js CHANGED
@@ -81,3 +81,24 @@ export async function fetchThreadHistory({ baseUrl, threadId, requestInit, inclu
81
81
  ...(includeCards ? { cards: threadHistoryRowsToCards(rows) } : {}),
82
82
  };
83
83
  }
84
+ /**
85
+ * The tool name for a `tool-result` block, read off its paired `tool-call`
86
+ * block in the same message (the reducer keeps both in one message's content).
87
+ * Falls back to `"tool"` when the pair is missing.
88
+ */
89
+ export function toolNameFor(message, toolCallId) {
90
+ for (const b of message.content) {
91
+ if (b.type === "tool-call" && b.toolCallId === toolCallId)
92
+ return b.name;
93
+ }
94
+ return "tool";
95
+ }
96
+ /**
97
+ * Persisted cards, ascending by transcript `seq` (stable; input untouched).
98
+ * These are PRIOR-turn cards — they always precede the live fold, so a
99
+ * block-preserving renderer surfaces them first (e.g. under an "Earlier in
100
+ * this conversation" divider).
101
+ */
102
+ export function sortHistoryCards(cards) {
103
+ return [...cards].sort((a, b) => a.seq - b.seq);
104
+ }
package/dist/index.d.ts CHANGED
@@ -1,11 +1,9 @@
1
1
  export { parseSseEvents, extractAssistantText, reduceAssistantText, stringField, parseConsentRequest, parseLinkRequest, type ParsedSseEvent, } from "./sse";
2
2
  export { dismissLinkPrompt } from "./link-prompt";
3
- export { createWebAdapters, localStorageThreadStore, webGenerateId, fetchStreamTransport, AgentResponseError, type CreateWebAdaptersOptions, } from "./web-adapters";
3
+ export { createUiResourceReader, type CreateUiResourceReaderOptions, createWebAdapters, localStorageThreadStore, webGenerateId, fetchStreamTransport, AgentResponseError, type CreateWebAdaptersOptions, } from "./web-adapters";
4
4
  export { fetchThreadHistory, threadHistoryRowsToMessages, threadHistoryRowsToCards, HistoryUnauthorizedError, type ThreadHistoryRow, type ThreadHistoryFetchOptions, } from "./history";
5
5
  export { ingestMessageFrame } from "./blocks";
6
- export { asResourcePayload, asUiResource, blockUiResource, cardUiResource, isJsonObject, resourceHtml, scanProviderRawForUiResource, sortHistoryCards, toolNameFor, toolResultUiResource, type McpUiResourcePayload, } from "./block-ui";
7
- export { asGguiRender, asGguiRenderBootstrap, blockGguiRender, gguiRenderResource, gguiShellHtml, toolResultGguiRender, GGUI_RENDER_META_KEY, type GguiRenderBootstrap, type GguiRenderDescriptor, type GguiShellHtmlOptions, } from "./ggui-render";
8
- export { cardCardMount, toolResultCardMount, type CardMount, type CardMountChannel, } from "./card-mount";
6
+ export { sortHistoryCards, toolNameFor } from "./history";
9
7
  export type { AgEvent, AgReduceResult, AgMessage, AgBlock } from "@silverprotocol/core";
10
8
  export type { AgentMessage, HistoryCard, ProfileConsentRequest, ProfileLinkRequest, ThreadIdStore, GenerateId, InvokeRequest, InvokeTransport, AgentInvokeAdapters, AgentInvokeHistoryAdapter, AgentInvokeStatus, HistoryLoadResult, UseAgentInvokeOptions, UseAgentInvokeReturn, } from "./types";
11
9
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,cAAc,EACd,oBAAoB,EACpB,mBAAmB,EACnB,WAAW,EACX,mBAAmB,EACnB,gBAAgB,EAChB,KAAK,cAAc,GACpB,MAAM,OAAO,CAAC;AACf,OAAO,EAAE,iBAAiB,EAAE,MAAM,eAAe,CAAC;AAClD,OAAO,EACL,iBAAiB,EACjB,uBAAuB,EACvB,aAAa,EACb,oBAAoB,EACpB,kBAAkB,EAClB,KAAK,wBAAwB,GAC9B,MAAM,gBAAgB,CAAC;AACxB,OAAO,EACL,kBAAkB,EAClB,2BAA2B,EAC3B,wBAAwB,EACxB,wBAAwB,EACxB,KAAK,gBAAgB,EACrB,KAAK,yBAAyB,GAC/B,MAAM,WAAW,CAAC;AACnB,OAAO,EAAE,kBAAkB,EAAE,MAAM,UAAU,CAAC;AAG9C,OAAO,EACL,iBAAiB,EACjB,YAAY,EACZ,eAAe,EACf,cAAc,EACd,YAAY,EACZ,YAAY,EACZ,4BAA4B,EAC5B,gBAAgB,EAChB,WAAW,EACX,oBAAoB,EACpB,KAAK,oBAAoB,GAC1B,MAAM,YAAY,CAAC;AAIpB,OAAO,EACL,YAAY,EACZ,qBAAqB,EACrB,eAAe,EACf,kBAAkB,EAClB,aAAa,EACb,oBAAoB,EACpB,oBAAoB,EACpB,KAAK,mBAAmB,EACxB,KAAK,oBAAoB,EACzB,KAAK,oBAAoB,GAC1B,MAAM,eAAe,CAAC;AACvB,OAAO,EACL,aAAa,EACb,mBAAmB,EACnB,KAAK,SAAS,EACd,KAAK,gBAAgB,GACtB,MAAM,cAAc,CAAC;AAItB,YAAY,EAAE,OAAO,EAAE,cAAc,EAAE,SAAS,EAAE,OAAO,EAAE,MAAM,sBAAsB,CAAC;AACxF,YAAY,EACV,YAAY,EACZ,WAAW,EACX,qBAAqB,EACrB,kBAAkB,EAClB,aAAa,EACb,UAAU,EACV,aAAa,EACb,eAAe,EACf,mBAAmB,EACnB,yBAAyB,EACzB,iBAAiB,EACjB,iBAAiB,EACjB,qBAAqB,EACrB,oBAAoB,GACrB,MAAM,SAAS,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,cAAc,EACd,oBAAoB,EACpB,mBAAmB,EACnB,WAAW,EACX,mBAAmB,EACnB,gBAAgB,EAChB,KAAK,cAAc,GACpB,MAAM,OAAO,CAAC;AACf,OAAO,EAAE,iBAAiB,EAAE,MAAM,eAAe,CAAC;AAClD,OAAO,EACL,sBAAsB,EACtB,KAAK,6BAA6B,EAClC,iBAAiB,EACjB,uBAAuB,EACvB,aAAa,EACb,oBAAoB,EACpB,kBAAkB,EAClB,KAAK,wBAAwB,GAC9B,MAAM,gBAAgB,CAAC;AACxB,OAAO,EACL,kBAAkB,EAClB,2BAA2B,EAC3B,wBAAwB,EACxB,wBAAwB,EACxB,KAAK,gBAAgB,EACrB,KAAK,yBAAyB,GAC/B,MAAM,WAAW,CAAC;AACnB,OAAO,EAAE,kBAAkB,EAAE,MAAM,UAAU,CAAC;AAK9C,OAAO,EAAE,gBAAgB,EAAE,WAAW,EAAE,MAAM,WAAW,CAAC;AAI1D,YAAY,EAAE,OAAO,EAAE,cAAc,EAAE,SAAS,EAAE,OAAO,EAAE,MAAM,sBAAsB,CAAC;AACxF,YAAY,EACV,YAAY,EACZ,WAAW,EACX,qBAAqB,EACrB,kBAAkB,EAClB,aAAa,EACb,UAAU,EACV,aAAa,EACb,eAAe,EACf,mBAAmB,EACnB,yBAAyB,EACzB,iBAAiB,EACjB,iBAAiB,EACjB,qBAAqB,EACrB,oBAAoB,GACrB,MAAM,SAAS,CAAC"}
package/dist/index.js CHANGED
@@ -1,13 +1,10 @@
1
1
  export { parseSseEvents, extractAssistantText, reduceAssistantText, stringField, parseConsentRequest, parseLinkRequest, } from "./sse";
2
2
  export { dismissLinkPrompt } from "./link-prompt";
3
- export { createWebAdapters, localStorageThreadStore, webGenerateId, fetchStreamTransport, AgentResponseError, } from "./web-adapters";
3
+ export { createUiResourceReader, createWebAdapters, localStorageThreadStore, webGenerateId, fetchStreamTransport, AgentResponseError, } from "./web-adapters";
4
4
  export { fetchThreadHistory, threadHistoryRowsToMessages, threadHistoryRowsToCards, HistoryUnauthorizedError, } from "./history";
5
5
  export { ingestMessageFrame } from "./blocks";
6
6
  // Pure block-walk / resource-narrowing helpers for a block-preserving renderer
7
7
  // (shared by Studio's `AgentBlocks` and Portal-web's agent chat). React-free.
8
- export { asResourcePayload, asUiResource, blockUiResource, cardUiResource, isJsonObject, resourceHtml, scanProviderRawForUiResource, sortHistoryCards, toolNameFor, toolResultUiResource, } from "./block-ui";
9
- // The ggui render channel (`uiData.resourceUri` + the `_meta["ai.ggui/render"]`
10
- // bootstrap) and the dispatcher that mounts EITHER channel through the same
11
- // second-origin sandbox path.
12
- export { asGguiRender, asGguiRenderBootstrap, blockGguiRender, gguiRenderResource, gguiShellHtml, toolResultGguiRender, GGUI_RENDER_META_KEY, } from "./ggui-render";
13
- export { cardCardMount, toolResultCardMount, } from "./card-mount";
8
+ // Transcript labeling/ordering helpers (mount narrowing itself moved to
9
+ // @guuey/mcp-apps-host the SEP-1865 Host role package; import it directly).
10
+ export { sortHistoryCards, toolNameFor } from "./history";
package/dist/sse.d.ts CHANGED
@@ -22,6 +22,11 @@ export declare function parseSseEvents(buffer: string): {
22
22
  * messages APPEND (streaming). The success `result` message carries the SAME
23
23
  * final text, so it REPLACES rather than appends — otherwise the answer renders
24
24
  * twice. The result is also the fallback for result-only turns.
25
+ *
26
+ * Block boundaries become paragraph breaks: a silver `text.start` frame (and,
27
+ * on the bypass arm, each new `assistant` message) opens a new block, so the
28
+ * separator lands exactly there — never per `text.delta`, which must stay a
29
+ * raw append for streaming (#91).
25
30
  */
26
31
  export declare function reduceAssistantText(current: string, data: unknown): string;
27
32
  /**
package/dist/sse.d.ts.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"sse.d.ts","sourceRoot":"","sources":["../src/sse.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,KAAK,EAAE,qBAAqB,EAAE,kBAAkB,EAAE,MAAM,SAAS,CAAC;AAEzE,MAAM,WAAW,cAAc;IAC7B,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,EAAE,OAAO,CAAC;CACf;AAED;;;;GAIG;AACH,wBAAgB,cAAc,CAAC,MAAM,EAAE,MAAM,GAAG;IAAE,MAAM,EAAE,cAAc,EAAE,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,CAqBzF;AAUD;;;;;GAKG;AACH,wBAAgB,mBAAmB,CAAC,OAAO,EAAE,MAAM,EAAE,IAAI,EAAE,OAAO,GAAG,MAAM,CAM1E;AAED;;;;;;;;;;;;;GAaG;AACH,wBAAgB,oBAAoB,CAAC,IAAI,EAAE,OAAO,GAAG,MAAM,CAgC1D;AAED,kEAAkE;AAClE,wBAAgB,WAAW,CAAC,IAAI,EAAE,OAAO,EAAE,GAAG,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAI1E;AAED;;;;;;;GAOG;AACH,wBAAgB,mBAAmB,CAAC,IAAI,EAAE,OAAO,GAAG,qBAAqB,GAAG,IAAI,CAO/E;AAED;;;;;;;GAOG;AACH,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,OAAO,GAAG,kBAAkB,GAAG,IAAI,CAOzE"}
1
+ {"version":3,"file":"sse.d.ts","sourceRoot":"","sources":["../src/sse.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,KAAK,EAAE,qBAAqB,EAAE,kBAAkB,EAAE,MAAM,SAAS,CAAC;AAEzE,MAAM,WAAW,cAAc;IAC7B,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,EAAE,OAAO,CAAC;CACf;AAED;;;;GAIG;AACH,wBAAgB,cAAc,CAAC,MAAM,EAAE,MAAM,GAAG;IAAE,MAAM,EAAE,cAAc,EAAE,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,CAqBzF;AAsBD;;;;;;;;;;GAUG;AACH,wBAAgB,mBAAmB,CAAC,OAAO,EAAE,MAAM,EAAE,IAAI,EAAE,OAAO,GAAG,MAAM,CAU1E;AAED;;;;;;;;;;;;;GAaG;AACH,wBAAgB,oBAAoB,CAAC,IAAI,EAAE,OAAO,GAAG,MAAM,CAmC1D;AAED,kEAAkE;AAClE,wBAAgB,WAAW,CAAC,IAAI,EAAE,OAAO,EAAE,GAAG,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAI1E;AAED;;;;;;;GAOG;AACH,wBAAgB,mBAAmB,CAAC,IAAI,EAAE,OAAO,GAAG,qBAAqB,GAAG,IAAI,CAO/E;AAED;;;;;;;GAOG;AACH,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,OAAO,GAAG,kBAAkB,GAAG,IAAI,CAOzE"}
package/dist/sse.js CHANGED
@@ -34,23 +34,49 @@ export function parseSseEvents(buffer) {
34
34
  }
35
35
  return { events, rest };
36
36
  }
37
+ function frameType(data) {
38
+ if (typeof data !== "object" || data === null)
39
+ return undefined;
40
+ return data.type;
41
+ }
37
42
  function isResultMessage(data) {
38
- return (typeof data === "object" &&
39
- data !== null &&
40
- data.type === "result");
43
+ return frameType(data) === "result";
44
+ }
45
+ /**
46
+ * Join point where a NEW text block begins: distinct blocks are distinct
47
+ * paragraphs on the flat text surface (guuey#98 — raw concatenation rendered
48
+ * "…instead.Here's your packing…"). Idempotent so an empty block between two
49
+ * boundaries never stacks separators.
50
+ */
51
+ function withBlockBreak(current) {
52
+ if (!current || current.endsWith("\n\n"))
53
+ return current;
54
+ return current + "\n\n";
41
55
  }
42
56
  /**
43
57
  * Fold one `message` SSE payload into the running assistant text. `assistant`
44
58
  * messages APPEND (streaming). The success `result` message carries the SAME
45
59
  * final text, so it REPLACES rather than appends — otherwise the answer renders
46
60
  * twice. The result is also the fallback for result-only turns.
61
+ *
62
+ * Block boundaries become paragraph breaks: a silver `text.start` frame (and,
63
+ * on the bypass arm, each new `assistant` message) opens a new block, so the
64
+ * separator lands exactly there — never per `text.delta`, which must stay a
65
+ * raw append for streaming (#91).
47
66
  */
48
67
  export function reduceAssistantText(current, data) {
49
68
  if (isResultMessage(data)) {
50
69
  const result = extractAssistantText(data);
51
70
  return result || current;
52
71
  }
53
- return current + extractAssistantText(data);
72
+ if (frameType(data) === "text.start")
73
+ return withBlockBreak(current);
74
+ const text = extractAssistantText(data);
75
+ if (!text)
76
+ return current;
77
+ if (frameType(data) === "assistant")
78
+ return withBlockBreak(current) + text;
79
+ return current + text;
54
80
  }
55
81
  /**
56
82
  * Best-effort assistant text from one `message` payload — BOTH protocols:
@@ -79,13 +105,16 @@ export function extractAssistantText(data) {
79
105
  if (typeof message === "object" && message !== null && "content" in message) {
80
106
  const content = message.content;
81
107
  if (Array.isArray(content)) {
108
+ // Distinct text blocks are distinct paragraphs (guuey#98) — same
109
+ // block-boundary contract as the silver arm's text.start handling.
82
110
  return content
83
111
  .filter((b) => typeof b === "object" &&
84
112
  b !== null &&
85
113
  b.type === "text" &&
86
114
  typeof b.text === "string")
87
115
  .map((b) => b.text)
88
- .join("");
116
+ .filter((t) => t.length > 0)
117
+ .join("\n\n");
89
118
  }
90
119
  }
91
120
  return "";
@@ -6,6 +6,7 @@
6
6
  * functions — never at module load — so this file is import-safe under SSR
7
7
  * (the functions guard on `typeof window`).
8
8
  */
9
+ import { type ResolvedViewMount } from "@guuey/mcp-apps-host";
9
10
  import type { AgentInvokeAdapters, InvokeRequest, ThreadIdStore } from "./types";
10
11
  /**
11
12
  * Thrown when the pod returns a non-2xx status on `/agent/invoke` (before any
@@ -120,4 +121,34 @@ export interface CreateWebAdaptersOptions {
120
121
  * cookie-only, history-less bundle.
121
122
  */
122
123
  export declare function createWebAdapters(opts?: CreateWebAdaptersOptions): AgentInvokeAdapters;
124
+ /** Options for {@link createUiResourceReader} — same credential surface as the history adapter. */
125
+ export interface CreateUiResourceReaderOptions {
126
+ /** The guuey public API base (`…/v1`). */
127
+ apiBaseUrl: string;
128
+ /** The thread whose persisted locators this reader may resolve. */
129
+ threadId: string;
130
+ /** Signed-in bearer — wins over the guest secret (same rule as the transport). */
131
+ getAccessToken?: (opts?: {
132
+ forceRefresh?: boolean;
133
+ }) => Promise<string | null>;
134
+ /** Caller-owned anonymous guest secret (widget / guest chat). */
135
+ guestSecret?: string | null;
136
+ /** Injectable for tests. */
137
+ fetchImpl?: typeof fetch;
138
+ }
139
+ /**
140
+ * Build a `UiResourceReader` over guuey's authenticated resources/read proxy
141
+ * (guuey#122 Gap 1: `GET /v1/threads/:threadId/ui-resource?uri=…`).
142
+ *
143
+ * This is `@guuey/mcp-apps-host`'s `createMcpUiResourceReader` assembly over
144
+ * a guuey-platform transport (guuey#127) — channel resolution and payload
145
+ * narrowing live in the host package; only the transport is guuey-shaped.
146
+ * The proxy owns EVERYTHING trust-shaped: caller identity (the same three
147
+ * families as the history read), thread ownership, the locator-to-thread
148
+ * scope guard, and the per-user federation mint. This transport only carries
149
+ * the surface's existing credential and maps EVERY non-OK — 401/403/404/502
150
+ * alike — to `undefined`: deny is byte-identical to a miss, and a miss
151
+ * renders the host's placeholder, never an error surface.
152
+ */
153
+ export declare function createUiResourceReader(options: CreateUiResourceReaderOptions): (resourceUri: string) => Promise<ResolvedViewMount | undefined>;
123
154
  //# sourceMappingURL=web-adapters.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"web-adapters.d.ts","sourceRoot":"","sources":["../src/web-adapters.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AACH,OAAO,KAAK,EACV,mBAAmB,EACnB,aAAa,EAEb,aAAa,EACd,MAAM,SAAS,CAAC;AAGjB;;;;;;GAMG;AACH,qBAAa,kBAAmB,SAAQ,KAAK;IAGzC,QAAQ,CAAC,MAAM,EAAE,MAAM;IACvB,QAAQ,CAAC,IAAI,CAAC,EAAE,MAAM;gBAFtB,OAAO,EAAE,MAAM,EACN,MAAM,EAAE,MAAM,EACd,IAAI,CAAC,EAAE,MAAM,YAAA;CAKzB;AAED,sEAAsE;AACtE,eAAO,MAAM,uBAAuB,EAAE,aAiBrC,CAAC;AAEF,mEAAmE;AACnE,wBAAgB,aAAa,IAAI,MAAM,CAKtC;AAwCD;;;;;;;;;;;;;;;;;;GAkBG;AACH,wBAAuB,oBAAoB,CACzC,GAAG,EAAE,aAAa,EAClB,WAAW,CAAC,EAAE,MAAM,GAAG,IAAI,EAC3B,WAAW,CAAC,EAAE,MAAM,GAAG,IAAI,GAC1B,cAAc,CAAC,MAAM,CAAC,CA4CxB;AAED,MAAM,WAAW,wBAAwB;IACvC;;;OAGG;IACH,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB;;;;;;;;;;;;;;;;;OAiBG;IACH,cAAc,CAAC,EAAE,CAAC,IAAI,CAAC,EAAE;QAAE,YAAY,CAAC,EAAE,OAAO,CAAA;KAAE,KAAK,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAAC;IAC/E;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;OAuCG;IACH,cAAc,CAAC,EAAE,MAAM,MAAM,GAAG,IAAI,CAAC;CACtC;AAED;;;;;;GAMG;AACH,wBAAgB,iBAAiB,CAC/B,IAAI,GAAE,wBAA6B,GAClC,mBAAmB,CAwErB"}
1
+ {"version":3,"file":"web-adapters.d.ts","sourceRoot":"","sources":["../src/web-adapters.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AACH,OAAO,EAGL,KAAK,iBAAiB,EACvB,MAAM,sBAAsB,CAAC;AAC9B,OAAO,KAAK,EACV,mBAAmB,EACnB,aAAa,EAEb,aAAa,EACd,MAAM,SAAS,CAAC;AAGjB;;;;;;GAMG;AACH,qBAAa,kBAAmB,SAAQ,KAAK;IAGzC,QAAQ,CAAC,MAAM,EAAE,MAAM;IACvB,QAAQ,CAAC,IAAI,CAAC,EAAE,MAAM;gBAFtB,OAAO,EAAE,MAAM,EACN,MAAM,EAAE,MAAM,EACd,IAAI,CAAC,EAAE,MAAM,YAAA;CAKzB;AAED,sEAAsE;AACtE,eAAO,MAAM,uBAAuB,EAAE,aAiBrC,CAAC;AAEF,mEAAmE;AACnE,wBAAgB,aAAa,IAAI,MAAM,CAKtC;AAwCD;;;;;;;;;;;;;;;;;;GAkBG;AACH,wBAAuB,oBAAoB,CACzC,GAAG,EAAE,aAAa,EAClB,WAAW,CAAC,EAAE,MAAM,GAAG,IAAI,EAC3B,WAAW,CAAC,EAAE,MAAM,GAAG,IAAI,GAC1B,cAAc,CAAC,MAAM,CAAC,CA4CxB;AAED,MAAM,WAAW,wBAAwB;IACvC;;;OAGG;IACH,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB;;;;;;;;;;;;;;;;;OAiBG;IACH,cAAc,CAAC,EAAE,CAAC,IAAI,CAAC,EAAE;QAAE,YAAY,CAAC,EAAE,OAAO,CAAA;KAAE,KAAK,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAAC;IAC/E;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;OAuCG;IACH,cAAc,CAAC,EAAE,MAAM,MAAM,GAAG,IAAI,CAAC;CACtC;AAED;;;;;;GAMG;AACH,wBAAgB,iBAAiB,CAC/B,IAAI,GAAE,wBAA6B,GAClC,mBAAmB,CAwErB;AAED,mGAAmG;AACnG,MAAM,WAAW,6BAA6B;IAC5C,0CAA0C;IAC1C,UAAU,EAAE,MAAM,CAAC;IACnB,mEAAmE;IACnE,QAAQ,EAAE,MAAM,CAAC;IACjB,kFAAkF;IAClF,cAAc,CAAC,EAAE,CAAC,IAAI,CAAC,EAAE;QAAE,YAAY,CAAC,EAAE,OAAO,CAAA;KAAE,KAAK,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAAC;IAC/E,iEAAiE;IACjE,WAAW,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC5B,4BAA4B;IAC5B,SAAS,CAAC,EAAE,OAAO,KAAK,CAAC;CAC1B;AAED;;;;;;;;;;;;;GAaG;AACH,wBAAgB,sBAAsB,CACpC,OAAO,EAAE,6BAA6B,GACrC,CAAC,WAAW,EAAE,MAAM,KAAK,OAAO,CAAC,iBAAiB,GAAG,SAAS,CAAC,CAoDjE"}
@@ -1,3 +1,12 @@
1
+ /**
2
+ * Web (browser / Next.js) host adapters for {@link useAgentInvoke}.
3
+ *
4
+ * Studio builds its bundle via {@link createWebAdapters}. The implementations
5
+ * touch `window.localStorage`, `crypto`, and `fetch` only inside their
6
+ * functions — never at module load — so this file is import-safe under SSR
7
+ * (the functions guard on `typeof window`).
8
+ */
9
+ import { createMcpUiResourceReader, } from "@guuey/mcp-apps-host";
1
10
  import { fetchThreadHistory, HistoryUnauthorizedError } from "./history";
2
11
  /**
3
12
  * Thrown when the pod returns a non-2xx status on `/agent/invoke` (before any
@@ -227,3 +236,77 @@ export function createWebAdapters(opts = {}) {
227
236
  }
228
237
  return adapters;
229
238
  }
239
+ /**
240
+ * Build a `UiResourceReader` over guuey's authenticated resources/read proxy
241
+ * (guuey#122 Gap 1: `GET /v1/threads/:threadId/ui-resource?uri=…`).
242
+ *
243
+ * This is `@guuey/mcp-apps-host`'s `createMcpUiResourceReader` assembly over
244
+ * a guuey-platform transport (guuey#127) — channel resolution and payload
245
+ * narrowing live in the host package; only the transport is guuey-shaped.
246
+ * The proxy owns EVERYTHING trust-shaped: caller identity (the same three
247
+ * families as the history read), thread ownership, the locator-to-thread
248
+ * scope guard, and the per-user federation mint. This transport only carries
249
+ * the surface's existing credential and maps EVERY non-OK — 401/403/404/502
250
+ * alike — to `undefined`: deny is byte-identical to a miss, and a miss
251
+ * renders the host's placeholder, never an error surface.
252
+ */
253
+ export function createUiResourceReader(options) {
254
+ const fetchImpl = options.fetchImpl ?? fetch;
255
+ const readResource = async (resourceUri) => {
256
+ const headers = {};
257
+ const token = options.getAccessToken ? await options.getAccessToken() : null;
258
+ const guest = sendableGuestSecret(options.guestSecret);
259
+ if (token) {
260
+ headers["authorization"] = `Bearer ${token}`;
261
+ }
262
+ else if (guest) {
263
+ headers[GUEST_HEADER] = guest;
264
+ }
265
+ const requestUrl = `${options.apiBaseUrl}/threads/${encodeURIComponent(options.threadId)}/ui-resource?uri=${encodeURIComponent(resourceUri)}`;
266
+ let res;
267
+ try {
268
+ res = await fetchImpl(requestUrl, { headers });
269
+ }
270
+ catch {
271
+ return undefined; // transport failure == miss == placeholder
272
+ }
273
+ // One forceRefresh retry on 401 with a bearer in play — the same
274
+ // expired-but-refreshable recovery the history adapter performs;
275
+ // without it a stale token degrades to a permanent placeholder.
276
+ if (res.status === 401 && options.getAccessToken) {
277
+ const fresh = await options.getAccessToken({ forceRefresh: true }).catch(() => null);
278
+ if (fresh) {
279
+ try {
280
+ res = await fetchImpl(requestUrl, {
281
+ headers: { ...headers, authorization: `Bearer ${fresh}` },
282
+ });
283
+ }
284
+ catch {
285
+ return undefined;
286
+ }
287
+ }
288
+ }
289
+ if (!res.ok)
290
+ return undefined;
291
+ let body;
292
+ try {
293
+ body = (await res.json());
294
+ }
295
+ catch {
296
+ return undefined;
297
+ }
298
+ // The proxy passes the blob arm through (a blob-only resource is not
299
+ // silently a miss — its route contract); mirror that here.
300
+ if (typeof body.uri !== "string")
301
+ return undefined;
302
+ if (typeof body.text !== "string" && typeof body.blob !== "string")
303
+ return undefined;
304
+ return {
305
+ uri: body.uri,
306
+ ...(typeof body.mimeType === "string" ? { mimeType: body.mimeType } : {}),
307
+ ...(typeof body.text === "string" ? { text: body.text } : {}),
308
+ ...(typeof body.blob === "string" ? { blob: body.blob } : {}),
309
+ };
310
+ };
311
+ return createMcpUiResourceReader({ readResource });
312
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@guuey/agent-client",
3
- "version": "0.2.3",
3
+ "version": "0.3.1",
4
4
  "description": "Client SDK for Guuey's agent runtime: the `useAgentInvoke` React hook + pure SSE helpers that speak the /agent/invoke streaming contract, plus the paginated thread-history read plane. Host adapters (storage / id / transport) are injected, so it runs on web (Next) and React Native alike.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -28,8 +28,8 @@
28
28
  }
29
29
  },
30
30
  "dependencies": {
31
- "@ggui-ai/protocol": "0.6.3",
32
- "@silverprotocol/core": "0.4.1"
31
+ "@silverprotocol/core": "0.4.1",
32
+ "@guuey/mcp-apps-host": "0.3.1"
33
33
  },
34
34
  "peerDependencies": {
35
35
  "react": ">=18"
package/src/history.ts CHANGED
@@ -10,7 +10,7 @@
10
10
  * `credentials: "include"`). Consumed by {@link createWebAdapters}; Portal
11
11
  * has its own copy today and can migrate onto this later.
12
12
  */
13
- import type { JsonValue } from "@silverprotocol/core";
13
+ import type { AgMessage, JsonValue } from "@silverprotocol/core";
14
14
  import type { AgentMessage, HistoryCard, HistoryLoadResult } from "./types";
15
15
 
16
16
  /** One row of `GET /v1/threads/:id/messages`. */
@@ -141,3 +141,25 @@ export async function fetchThreadHistory({
141
141
  ...(includeCards ? { cards: threadHistoryRowsToCards(rows) } : {}),
142
142
  };
143
143
  }
144
+
145
+ /**
146
+ * The tool name for a `tool-result` block, read off its paired `tool-call`
147
+ * block in the same message (the reducer keeps both in one message's content).
148
+ * Falls back to `"tool"` when the pair is missing.
149
+ */
150
+ export function toolNameFor(message: AgMessage, toolCallId: string): string {
151
+ for (const b of message.content) {
152
+ if (b.type === "tool-call" && b.toolCallId === toolCallId) return b.name;
153
+ }
154
+ return "tool";
155
+ }
156
+
157
+ /**
158
+ * Persisted cards, ascending by transcript `seq` (stable; input untouched).
159
+ * These are PRIOR-turn cards — they always precede the live fold, so a
160
+ * block-preserving renderer surfaces them first (e.g. under an "Earlier in
161
+ * this conversation" divider).
162
+ */
163
+ export function sortHistoryCards(cards: readonly HistoryCard[]): HistoryCard[] {
164
+ return [...cards].sort((a, b) => a.seq - b.seq);
165
+ }
package/src/index.ts CHANGED
@@ -9,6 +9,8 @@ export {
9
9
  } from "./sse";
10
10
  export { dismissLinkPrompt } from "./link-prompt";
11
11
  export {
12
+ createUiResourceReader,
13
+ type CreateUiResourceReaderOptions,
12
14
  createWebAdapters,
13
15
  localStorageThreadStore,
14
16
  webGenerateId,
@@ -27,40 +29,9 @@ export {
27
29
  export { ingestMessageFrame } from "./blocks";
28
30
  // Pure block-walk / resource-narrowing helpers for a block-preserving renderer
29
31
  // (shared by Studio's `AgentBlocks` and Portal-web's agent chat). React-free.
30
- export {
31
- asResourcePayload,
32
- asUiResource,
33
- blockUiResource,
34
- cardUiResource,
35
- isJsonObject,
36
- resourceHtml,
37
- scanProviderRawForUiResource,
38
- sortHistoryCards,
39
- toolNameFor,
40
- toolResultUiResource,
41
- type McpUiResourcePayload,
42
- } from "./block-ui";
43
- // The ggui render channel (`uiData.resourceUri` + the `_meta["ai.ggui/render"]`
44
- // bootstrap) and the dispatcher that mounts EITHER channel through the same
45
- // second-origin sandbox path.
46
- export {
47
- asGguiRender,
48
- asGguiRenderBootstrap,
49
- blockGguiRender,
50
- gguiRenderResource,
51
- gguiShellHtml,
52
- toolResultGguiRender,
53
- GGUI_RENDER_META_KEY,
54
- type GguiRenderBootstrap,
55
- type GguiRenderDescriptor,
56
- type GguiShellHtmlOptions,
57
- } from "./ggui-render";
58
- export {
59
- cardCardMount,
60
- toolResultCardMount,
61
- type CardMount,
62
- type CardMountChannel,
63
- } from "./card-mount";
32
+ // Transcript labeling/ordering helpers (mount narrowing itself moved to
33
+ // @guuey/mcp-apps-host — the SEP-1865 Host role package; import it directly).
34
+ export { sortHistoryCards, toolNameFor } from "./history";
64
35
  // Re-export the AgJSON types the block-preserving transcript surfaces, so
65
36
  // consumers can name `reduceResult` / block types without a direct
66
37
  // `@silverprotocol/core` import.
package/src/sse.ts CHANGED
@@ -39,12 +39,24 @@ export function parseSseEvents(buffer: string): { events: ParsedSseEvent[]; rest
39
39
  return { events, rest };
40
40
  }
41
41
 
42
+ function frameType(data: unknown): unknown {
43
+ if (typeof data !== "object" || data === null) return undefined;
44
+ return (data as { type?: unknown }).type;
45
+ }
46
+
42
47
  function isResultMessage(data: unknown): boolean {
43
- return (
44
- typeof data === "object" &&
45
- data !== null &&
46
- (data as { type?: unknown }).type === "result"
47
- );
48
+ return frameType(data) === "result";
49
+ }
50
+
51
+ /**
52
+ * Join point where a NEW text block begins: distinct blocks are distinct
53
+ * paragraphs on the flat text surface (guuey#98 — raw concatenation rendered
54
+ * "…instead.Here's your packing…"). Idempotent so an empty block between two
55
+ * boundaries never stacks separators.
56
+ */
57
+ function withBlockBreak(current: string): string {
58
+ if (!current || current.endsWith("\n\n")) return current;
59
+ return current + "\n\n";
48
60
  }
49
61
 
50
62
  /**
@@ -52,13 +64,22 @@ function isResultMessage(data: unknown): boolean {
52
64
  * messages APPEND (streaming). The success `result` message carries the SAME
53
65
  * final text, so it REPLACES rather than appends — otherwise the answer renders
54
66
  * twice. The result is also the fallback for result-only turns.
67
+ *
68
+ * Block boundaries become paragraph breaks: a silver `text.start` frame (and,
69
+ * on the bypass arm, each new `assistant` message) opens a new block, so the
70
+ * separator lands exactly there — never per `text.delta`, which must stay a
71
+ * raw append for streaming (#91).
55
72
  */
56
73
  export function reduceAssistantText(current: string, data: unknown): string {
57
74
  if (isResultMessage(data)) {
58
75
  const result = extractAssistantText(data);
59
76
  return result || current;
60
77
  }
61
- return current + extractAssistantText(data);
78
+ if (frameType(data) === "text.start") return withBlockBreak(current);
79
+ const text = extractAssistantText(data);
80
+ if (!text) return current;
81
+ if (frameType(data) === "assistant") return withBlockBreak(current) + text;
82
+ return current + text;
62
83
  }
63
84
 
64
85
  /**
@@ -87,6 +108,8 @@ export function extractAssistantText(data: unknown): string {
87
108
  if (typeof message === "object" && message !== null && "content" in message) {
88
109
  const content = (message as { content?: unknown }).content;
89
110
  if (Array.isArray(content)) {
111
+ // Distinct text blocks are distinct paragraphs (guuey#98) — same
112
+ // block-boundary contract as the silver arm's text.start handling.
90
113
  return content
91
114
  .filter(
92
115
  (b): b is { type: "text"; text: string } =>
@@ -96,7 +119,8 @@ export function extractAssistantText(data: unknown): string {
96
119
  typeof (b as { text?: unknown }).text === "string",
97
120
  )
98
121
  .map((b) => b.text)
99
- .join("");
122
+ .filter((t) => t.length > 0)
123
+ .join("\n\n");
100
124
  }
101
125
  }
102
126
  return "";
@@ -6,6 +6,11 @@
6
6
  * functions — never at module load — so this file is import-safe under SSR
7
7
  * (the functions guard on `typeof window`).
8
8
  */
9
+ import {
10
+ createMcpUiResourceReader,
11
+ type McpResourceReadResult,
12
+ type ResolvedViewMount,
13
+ } from "@guuey/mcp-apps-host";
9
14
  import type {
10
15
  AgentInvokeAdapters,
11
16
  InvokeRequest,
@@ -317,3 +322,87 @@ export function createWebAdapters(
317
322
 
318
323
  return adapters;
319
324
  }
325
+
326
+ /** Options for {@link createUiResourceReader} — same credential surface as the history adapter. */
327
+ export interface CreateUiResourceReaderOptions {
328
+ /** The guuey public API base (`…/v1`). */
329
+ apiBaseUrl: string;
330
+ /** The thread whose persisted locators this reader may resolve. */
331
+ threadId: string;
332
+ /** Signed-in bearer — wins over the guest secret (same rule as the transport). */
333
+ getAccessToken?: (opts?: { forceRefresh?: boolean }) => Promise<string | null>;
334
+ /** Caller-owned anonymous guest secret (widget / guest chat). */
335
+ guestSecret?: string | null;
336
+ /** Injectable for tests. */
337
+ fetchImpl?: typeof fetch;
338
+ }
339
+
340
+ /**
341
+ * Build a `UiResourceReader` over guuey's authenticated resources/read proxy
342
+ * (guuey#122 Gap 1: `GET /v1/threads/:threadId/ui-resource?uri=…`).
343
+ *
344
+ * This is `@guuey/mcp-apps-host`'s `createMcpUiResourceReader` assembly over
345
+ * a guuey-platform transport (guuey#127) — channel resolution and payload
346
+ * narrowing live in the host package; only the transport is guuey-shaped.
347
+ * The proxy owns EVERYTHING trust-shaped: caller identity (the same three
348
+ * families as the history read), thread ownership, the locator-to-thread
349
+ * scope guard, and the per-user federation mint. This transport only carries
350
+ * the surface's existing credential and maps EVERY non-OK — 401/403/404/502
351
+ * alike — to `undefined`: deny is byte-identical to a miss, and a miss
352
+ * renders the host's placeholder, never an error surface.
353
+ */
354
+ export function createUiResourceReader(
355
+ options: CreateUiResourceReaderOptions,
356
+ ): (resourceUri: string) => Promise<ResolvedViewMount | undefined> {
357
+ const fetchImpl = options.fetchImpl ?? fetch;
358
+ const readResource = async (resourceUri: string): Promise<McpResourceReadResult | undefined> => {
359
+ const headers: Record<string, string> = {};
360
+ const token = options.getAccessToken ? await options.getAccessToken() : null;
361
+ const guest = sendableGuestSecret(options.guestSecret);
362
+ if (token) {
363
+ headers["authorization"] = `Bearer ${token}`;
364
+ } else if (guest) {
365
+ headers[GUEST_HEADER] = guest;
366
+ }
367
+ const requestUrl = `${options.apiBaseUrl}/threads/${encodeURIComponent(options.threadId)}/ui-resource?uri=${encodeURIComponent(resourceUri)}`;
368
+ let res: Response;
369
+ try {
370
+ res = await fetchImpl(requestUrl, { headers });
371
+ } catch {
372
+ return undefined; // transport failure == miss == placeholder
373
+ }
374
+ // One forceRefresh retry on 401 with a bearer in play — the same
375
+ // expired-but-refreshable recovery the history adapter performs;
376
+ // without it a stale token degrades to a permanent placeholder.
377
+ if (res.status === 401 && options.getAccessToken) {
378
+ const fresh = await options.getAccessToken({ forceRefresh: true }).catch(() => null);
379
+ if (fresh) {
380
+ try {
381
+ res = await fetchImpl(requestUrl, {
382
+ headers: { ...headers, authorization: `Bearer ${fresh}` },
383
+ });
384
+ } catch {
385
+ return undefined;
386
+ }
387
+ }
388
+ }
389
+ if (!res.ok) return undefined;
390
+ let body: { uri?: unknown; mimeType?: unknown; text?: unknown; blob?: unknown };
391
+ try {
392
+ body = (await res.json()) as typeof body;
393
+ } catch {
394
+ return undefined;
395
+ }
396
+ // The proxy passes the blob arm through (a blob-only resource is not
397
+ // silently a miss — its route contract); mirror that here.
398
+ if (typeof body.uri !== "string") return undefined;
399
+ if (typeof body.text !== "string" && typeof body.blob !== "string") return undefined;
400
+ return {
401
+ uri: body.uri,
402
+ ...(typeof body.mimeType === "string" ? { mimeType: body.mimeType } : {}),
403
+ ...(typeof body.text === "string" ? { text: body.text } : {}),
404
+ ...(typeof body.blob === "string" ? { blob: body.blob } : {}),
405
+ };
406
+ };
407
+ return createMcpUiResourceReader({ readResource });
408
+ }