@guuey/agent-client 0.1.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.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Loqu, Inc.
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,64 @@
1
+ # @guuey/agent-client
2
+
3
+ The client SDK for a [guuey](https://guuey.com) agent's streaming contract:
4
+
5
+ - **`POST /agent/invoke`** — a Server-Sent-Events stream (`session` / `message`
6
+ / `done` / `error` frames) folded into a flat transcript.
7
+ - **`GET /threads/:id/messages`** — the paginated history read plane, so a
8
+ reload repaints the conversation before any new streaming starts.
9
+
10
+ The hook is platform-agnostic: thread-id storage, client-message-id
11
+ generation, and the network transport (which also carries identity) are
12
+ injected as adapters, so the same core runs on web (Next.js) and React Native.
13
+
14
+ ```bash
15
+ npm install @guuey/agent-client
16
+ ```
17
+
18
+ ## Two entry points
19
+
20
+ | Import | Contents | React? |
21
+ | --------------------------- | ---------------------------------------------------------------------------------- | ------ |
22
+ | `@guuey/agent-client` | SSE helpers, the thread-history reader, `createWebAdapters`, and all public types. | No |
23
+ | `@guuey/agent-client/react` | The `useAgentInvoke` hook (+ `applyHistoryResult`). | Yes |
24
+
25
+ The root subpath is React-free — importing it never pulls React in. React is a
26
+ **required peer** (`react >=18`) because the `./react` subpath needs it; if you
27
+ only consume the root subpath, that peer is inert at runtime.
28
+
29
+ ## React example
30
+
31
+ ```tsx
32
+ import { useAgentInvoke } from "@guuey/agent-client/react";
33
+ import { createWebAdapters } from "@guuey/agent-client";
34
+
35
+ export function Chat({ endpointUrl, appId }: { endpointUrl: string; appId: string }) {
36
+ const adapters = createWebAdapters({ getAccessToken: async () => myToken });
37
+ const { messages, send, isStreaming } = useAgentInvoke({ endpointUrl, appId, adapters });
38
+
39
+ return (
40
+ <>
41
+ {messages.map((m, i) => (
42
+ <p key={i} data-role={m.role}>
43
+ {m.text}
44
+ </p>
45
+ ))}
46
+ <button disabled={isStreaming} onClick={() => send("hello")}>
47
+ Send
48
+ </button>
49
+ </>
50
+ );
51
+ }
52
+ ```
53
+
54
+ On React Native, supply your own adapters (AsyncStorage + an `expo/fetch`
55
+ transport) in place of `createWebAdapters` — the hook's contract is identical.
56
+
57
+ ## React Native / Metro
58
+
59
+ Both entry points declare a `react-native` export condition that points at the
60
+ **TypeScript source** (shipped in the npm tarball alongside `dist/` for exactly
61
+ this reason — the standard RN package pattern). Metro resolves that condition
62
+ by default and transpiles the source with your app's Babel config; Node and
63
+ web bundlers ignore it and use the compiled ESM in `dist/`. No
64
+ `transpilePackages`-style configuration is needed on either side.
@@ -0,0 +1,112 @@
1
+ /**
2
+ * Pure block-walk / resource-narrowing helpers for a block-preserving agent
3
+ * transcript — no React, no DOM, so the narrowing logic stays unit-testable in
4
+ * isolation (this package's vitest runs a `node` environment) and can be shared
5
+ * by every host renderer (Studio's `AgentBlocks`, Portal-web's agent chat).
6
+ *
7
+ * The pod's AgJSON wire carries generative-UI payloads on `tool.done` events,
8
+ * which the reducer folds onto `tool-result` blocks. Two channels reach us:
9
+ *
10
+ * 1. **`uiData`** — the MCP-Apps *surface* channel. The pod's Claude facet
11
+ * routes a tool result's `structuredContent` here when the server stamped
12
+ * `_meta.ui`. Any resource here is intended as UI.
13
+ * 2. **`provider-raw` content blocks** — an MCP embedded `resource` content
14
+ * part does NOT survive as a first-class `resource` AgBlock in the Claude
15
+ * facet; it degrades to `{ type:'provider-raw', vendor, raw:<part> }`. So a
16
+ * `ui://` resource can be hiding inside `provider-raw.raw` and must be
17
+ * scanned for defensively.
18
+ *
19
+ * The resource-narrowing (opaque `JsonValue` → typed payload) mirrors the
20
+ * proven `create-agentic-app` web template — structural validation, never a cast.
21
+ */
22
+ import type { AgBlock, AgMessage, JsonValue } from "@silverprotocol/core";
23
+ import type { HistoryCard } from "./types";
24
+ /** A narrowed MCP embedded UI resource (the `_meta.ui.resource` shape). */
25
+ export interface McpUiResourcePayload {
26
+ uri: string;
27
+ mimeType?: string;
28
+ text?: string;
29
+ blob?: string;
30
+ }
31
+ /** Narrow an opaque `JsonValue` to a plain (non-array) JSON object. */
32
+ export declare function isJsonObject(v: JsonValue | undefined): v is {
33
+ [key: string]: JsonValue;
34
+ };
35
+ /**
36
+ * A JSON object → an MCP UI resource, if it has a `uri` plus renderable
37
+ * payload (`text` or base64 `blob`). Returns `undefined` for anything else.
38
+ */
39
+ export declare function asResourcePayload(v: JsonValue | undefined): McpUiResourcePayload | undefined;
40
+ /**
41
+ * Does a `tool-result` block's `uiData` carry an MCP embedded UI resource?
42
+ * Accepts the resource inlined directly, or wrapped as `{ resource: {...} }`
43
+ * (the shape an MCP `resource` content part carries). No `ui://` scheme gate
44
+ * here on purpose: `uiData` is the explicit *surface* channel (the server
45
+ * stamped `_meta.ui`), so any resource on it is meant to render.
46
+ */
47
+ export declare function asUiResource(uiData: JsonValue | undefined): McpUiResourcePayload | undefined;
48
+ /**
49
+ * Scan a `provider-raw` block's `raw` (the vendor tool_result content part)
50
+ * for a *generative-UI* resource. Unlike {@link asUiResource}, this path IS
51
+ * gated on the `ui://` scheme: `provider-raw` degradation is a lossy catch-all,
52
+ * so a plain file/text resource riding it is NOT a UI to mount — only the
53
+ * mcp-ui `ui://` convention is.
54
+ */
55
+ export declare function scanProviderRawForUiResource(raw: JsonValue | undefined): McpUiResourcePayload | undefined;
56
+ /**
57
+ * Extract a mountable UI resource from an opaque AgBlock-shaped `JsonValue`
58
+ * (used for persisted card snapshot parts, which arrive untyped). Dispatches
59
+ * by `block.type`:
60
+ * - `tool-result` → its `uiData` surface channel
61
+ * - `provider-raw` → a `ui://` resource hiding in `raw`
62
+ * - `resource` → a first-class embedded resource (gated on `ui://`)
63
+ * Everything else → `undefined`.
64
+ */
65
+ export declare function blockUiResource(block: JsonValue): McpUiResourcePayload | undefined;
66
+ /**
67
+ * A live `tool-result` AgBlock → its mountable UI resource, checking BOTH
68
+ * channels the Claude facet uses:
69
+ * 1. the `uiData` surface channel (server stamped `_meta.ui`), and
70
+ * 2. an embedded `ui://` resource that degraded into a `provider-raw`
71
+ * content part inside the tool result (MCP `resource` parts do NOT survive
72
+ * as first-class `resource` AgBlocks here).
73
+ * First-class `resource` content parts are intentionally not scanned in this
74
+ * typed live path (the Claude facet never emits them); the untyped card path
75
+ * ({@link blockUiResource}) covers them for other facets' persisted snapshots.
76
+ */
77
+ export declare function toolResultUiResource(block: Extract<AgBlock, {
78
+ type: "tool-result";
79
+ }>): McpUiResourcePayload | undefined;
80
+ /**
81
+ * A persisted `HistoryCard`'s `cardSnapshot` → a mountable UI resource. The
82
+ * snapshot is the verbatim `AgArtifact` the pod stored (`{ parts: AgBlock[] }`),
83
+ * so walk its `parts` for the first block that yields a resource; fall back to
84
+ * treating the snapshot root itself as a block.
85
+ *
86
+ * NOTE (`no-ggui-tools`): a ggui-rendered card carries NO inline HTML resource —
87
+ * its UI rides `_meta.ggui.bootstrap` and mounts via `@ggui-ai/react`'s
88
+ * `McpAppIframe`. That branch is OUT OF SCOPE for v1 (deferred-pending-capture).
89
+ * So a real ggui card resolves to `undefined` here and renders as the host's
90
+ * coherent placeholder, not a broken mount.
91
+ */
92
+ export declare function cardUiResource(cardSnapshot: JsonValue): McpUiResourcePayload | undefined;
93
+ /**
94
+ * The resource's HTML: inline `text` wins; else base64-decode `blob`. `atob`
95
+ * alone yields a Latin-1 string (mojibake on multibyte UTF-8), so decode via
96
+ * bytes + `TextDecoder`. Invalid base64 → `undefined` (no renderable payload).
97
+ */
98
+ export declare function resourceHtml(resource: McpUiResourcePayload): string | undefined;
99
+ /**
100
+ * The tool name for a `tool-result` block, read off its paired `tool-call`
101
+ * block in the same message (the reducer keeps both in one message's content).
102
+ * Falls back to `"tool"` when the pair is missing.
103
+ */
104
+ export declare function toolNameFor(message: AgMessage, toolCallId: string): string;
105
+ /**
106
+ * Persisted cards, ascending by transcript `seq` (stable; input untouched).
107
+ * These are PRIOR-turn cards — they always precede the live fold, so a
108
+ * block-preserving renderer surfaces them first (e.g. under an "Earlier in
109
+ * this conversation" divider).
110
+ */
111
+ export declare function sortHistoryCards(cards: readonly HistoryCard[]): HistoryCard[];
112
+ //# sourceMappingURL=block-ui.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"block-ui.d.ts","sourceRoot":"","sources":["../src/block-ui.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,OAAO,KAAK,EAAE,OAAO,EAAE,SAAS,EAAE,SAAS,EAAE,MAAM,sBAAsB,CAAC;AAC1E,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,SAAS,CAAC;AAE3C,2EAA2E;AAC3E,MAAM,WAAW,oBAAoB;IACnC,GAAG,EAAE,MAAM,CAAC;IACZ,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,IAAI,CAAC,EAAE,MAAM,CAAC;CACf;AAED,uEAAuE;AACvE,wBAAgB,YAAY,CAAC,CAAC,EAAE,SAAS,GAAG,SAAS,GAAG,CAAC,IAAI;IAAE,CAAC,GAAG,EAAE,MAAM,GAAG,SAAS,CAAA;CAAE,CAExF;AAED;;;GAGG;AACH,wBAAgB,iBAAiB,CAAC,CAAC,EAAE,SAAS,GAAG,SAAS,GAAG,oBAAoB,GAAG,SAAS,CAU5F;AAED;;;;;;GAMG;AACH,wBAAgB,YAAY,CAAC,MAAM,EAAE,SAAS,GAAG,SAAS,GAAG,oBAAoB,GAAG,SAAS,CAK5F;AAED;;;;;;GAMG;AACH,wBAAgB,4BAA4B,CAC1C,GAAG,EAAE,SAAS,GAAG,SAAS,GACzB,oBAAoB,GAAG,SAAS,CAMlC;AAED;;;;;;;;GAQG;AACH,wBAAgB,eAAe,CAAC,KAAK,EAAE,SAAS,GAAG,oBAAoB,GAAG,SAAS,CAclF;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,oBAAoB,CAClC,KAAK,EAAE,OAAO,CAAC,OAAO,EAAE;IAAE,IAAI,EAAE,aAAa,CAAA;CAAE,CAAC,GAC/C,oBAAoB,GAAG,SAAS,CAUlC;AAED;;;;;;;;;;;GAWG;AACH,wBAAgB,cAAc,CAAC,YAAY,EAAE,SAAS,GAAG,oBAAoB,GAAG,SAAS,CAUxF;AAED;;;;GAIG;AACH,wBAAgB,YAAY,CAAC,QAAQ,EAAE,oBAAoB,GAAG,MAAM,GAAG,SAAS,CAU/E;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"}
@@ -0,0 +1,165 @@
1
+ /** Narrow an opaque `JsonValue` to a plain (non-array) JSON object. */
2
+ export function isJsonObject(v) {
3
+ return typeof v === "object" && v !== null && !Array.isArray(v);
4
+ }
5
+ /**
6
+ * A JSON object → an MCP UI resource, if it has a `uri` plus renderable
7
+ * payload (`text` or base64 `blob`). Returns `undefined` for anything else.
8
+ */
9
+ export function asResourcePayload(v) {
10
+ if (!isJsonObject(v))
11
+ return undefined;
12
+ if (typeof v.uri !== "string")
13
+ return undefined;
14
+ if (typeof v.text !== "string" && typeof v.blob !== "string")
15
+ return undefined;
16
+ return {
17
+ uri: v.uri,
18
+ ...(typeof v.mimeType === "string" ? { mimeType: v.mimeType } : {}),
19
+ ...(typeof v.text === "string" ? { text: v.text } : {}),
20
+ ...(typeof v.blob === "string" ? { blob: v.blob } : {}),
21
+ };
22
+ }
23
+ /**
24
+ * Does a `tool-result` block's `uiData` carry an MCP embedded UI resource?
25
+ * Accepts the resource inlined directly, or wrapped as `{ resource: {...} }`
26
+ * (the shape an MCP `resource` content part carries). No `ui://` scheme gate
27
+ * here on purpose: `uiData` is the explicit *surface* channel (the server
28
+ * stamped `_meta.ui`), so any resource on it is meant to render.
29
+ */
30
+ export function asUiResource(uiData) {
31
+ if (!isJsonObject(uiData))
32
+ return undefined;
33
+ const direct = asResourcePayload(uiData);
34
+ if (direct)
35
+ return direct;
36
+ return asResourcePayload(uiData.resource);
37
+ }
38
+ /**
39
+ * Scan a `provider-raw` block's `raw` (the vendor tool_result content part)
40
+ * for a *generative-UI* resource. Unlike {@link asUiResource}, this path IS
41
+ * gated on the `ui://` scheme: `provider-raw` degradation is a lossy catch-all,
42
+ * so a plain file/text resource riding it is NOT a UI to mount — only the
43
+ * mcp-ui `ui://` convention is.
44
+ */
45
+ export function scanProviderRawForUiResource(raw) {
46
+ if (!isJsonObject(raw))
47
+ return undefined;
48
+ const candidate = raw.resource !== undefined ? asResourcePayload(raw.resource) : asResourcePayload(raw);
49
+ if (!candidate)
50
+ return undefined;
51
+ return candidate.uri.startsWith("ui://") ? candidate : undefined;
52
+ }
53
+ /**
54
+ * Extract a mountable UI resource from an opaque AgBlock-shaped `JsonValue`
55
+ * (used for persisted card snapshot parts, which arrive untyped). Dispatches
56
+ * by `block.type`:
57
+ * - `tool-result` → its `uiData` surface channel
58
+ * - `provider-raw` → a `ui://` resource hiding in `raw`
59
+ * - `resource` → a first-class embedded resource (gated on `ui://`)
60
+ * Everything else → `undefined`.
61
+ */
62
+ export function blockUiResource(block) {
63
+ if (!isJsonObject(block))
64
+ return undefined;
65
+ switch (block.type) {
66
+ case "tool-result":
67
+ return asUiResource(block.uiData);
68
+ case "provider-raw":
69
+ return scanProviderRawForUiResource(block.raw);
70
+ case "resource": {
71
+ const r = asResourcePayload(block.resource);
72
+ return r && r.uri.startsWith("ui://") ? r : undefined;
73
+ }
74
+ default:
75
+ return undefined;
76
+ }
77
+ }
78
+ /**
79
+ * A live `tool-result` AgBlock → its mountable UI resource, checking BOTH
80
+ * channels the Claude facet uses:
81
+ * 1. the `uiData` surface channel (server stamped `_meta.ui`), and
82
+ * 2. an embedded `ui://` resource that degraded into a `provider-raw`
83
+ * content part inside the tool result (MCP `resource` parts do NOT survive
84
+ * as first-class `resource` AgBlocks here).
85
+ * First-class `resource` content parts are intentionally not scanned in this
86
+ * typed live path (the Claude facet never emits them); the untyped card path
87
+ * ({@link blockUiResource}) covers them for other facets' persisted snapshots.
88
+ */
89
+ export function toolResultUiResource(block) {
90
+ const fromUiData = asUiResource(block.uiData);
91
+ if (fromUiData)
92
+ return fromUiData;
93
+ for (const part of block.content) {
94
+ if (part.type === "provider-raw") {
95
+ const found = scanProviderRawForUiResource(part.raw);
96
+ if (found)
97
+ return found;
98
+ }
99
+ }
100
+ return undefined;
101
+ }
102
+ /**
103
+ * A persisted `HistoryCard`'s `cardSnapshot` → a mountable UI resource. The
104
+ * snapshot is the verbatim `AgArtifact` the pod stored (`{ parts: AgBlock[] }`),
105
+ * so walk its `parts` for the first block that yields a resource; fall back to
106
+ * treating the snapshot root itself as a block.
107
+ *
108
+ * NOTE (`no-ggui-tools`): a ggui-rendered card carries NO inline HTML resource —
109
+ * its UI rides `_meta.ggui.bootstrap` and mounts via `@ggui-ai/react`'s
110
+ * `McpAppIframe`. That branch is OUT OF SCOPE for v1 (deferred-pending-capture).
111
+ * So a real ggui card resolves to `undefined` here and renders as the host's
112
+ * coherent placeholder, not a broken mount.
113
+ */
114
+ export function cardUiResource(cardSnapshot) {
115
+ if (!isJsonObject(cardSnapshot))
116
+ return undefined;
117
+ const parts = cardSnapshot.parts;
118
+ if (Array.isArray(parts)) {
119
+ for (const part of parts) {
120
+ const found = blockUiResource(part);
121
+ if (found)
122
+ return found;
123
+ }
124
+ }
125
+ return blockUiResource(cardSnapshot);
126
+ }
127
+ /**
128
+ * The resource's HTML: inline `text` wins; else base64-decode `blob`. `atob`
129
+ * alone yields a Latin-1 string (mojibake on multibyte UTF-8), so decode via
130
+ * bytes + `TextDecoder`. Invalid base64 → `undefined` (no renderable payload).
131
+ */
132
+ export function resourceHtml(resource) {
133
+ if (resource.text !== undefined)
134
+ return resource.text;
135
+ if (resource.blob !== undefined) {
136
+ try {
137
+ return new TextDecoder().decode(Uint8Array.from(atob(resource.blob), (c) => c.charCodeAt(0)));
138
+ }
139
+ catch {
140
+ return undefined;
141
+ }
142
+ }
143
+ return undefined;
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, toolCallId) {
151
+ for (const b of message.content) {
152
+ if (b.type === "tool-call" && b.toolCallId === toolCallId)
153
+ return b.name;
154
+ }
155
+ return "tool";
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) {
164
+ return [...cards].sort((a, b) => a.seq - b.seq);
165
+ }
@@ -0,0 +1,27 @@
1
+ /**
2
+ * Frame → AgEvent[] ingestion for the opt-in block-preserving transcript.
3
+ *
4
+ * The wire carries ONE `message` frame per SSE event, but the *payload* shape
5
+ * differs by producer:
6
+ *
7
+ * - the deployed pod (`nocode-runtime`) emits a SINGLE AgEvent JSON OBJECT per
8
+ * frame (`sendEvent(res, 'message', e)`);
9
+ * - the CLI dev server (`guuey dev --serve`) batches an AgEvent[] ARRAY per
10
+ * frame (`sendEvent(res, 'message', batch)`).
11
+ *
12
+ * `ingestMessageFrame` is tolerant of BOTH shapes (object OR array). Validation
13
+ * is delegated to `@silverprotocol/core`'s `ingestAgEvents`, the library's own
14
+ * parse-known-else-skip consumer validator: any element that is not a valid
15
+ * AgJSON event — including bypass-mode SDKMessage shapes (`type: "assistant"` /
16
+ * `type: "result"`, which are NOT AgEvent `type` literals) — is dropped rather
17
+ * than crashing the stream or forcing a type lie. A frame that is not even a
18
+ * JSON value (`undefined`, a function, `NaN`, …) yields `[]`.
19
+ *
20
+ * The `unknown` → `JsonValue` narrowing is done at runtime via the exported
21
+ * `JsonValue` schema's `safeParse` (the same posture the Claude facet uses for
22
+ * provider-raw parts) — no assertion, no `as`.
23
+ */
24
+ import { type AgEvent } from "@silverprotocol/core";
25
+ /** Ingest one `message` SSE payload (object OR array) into validated AgEvents. */
26
+ export declare function ingestMessageFrame(raw: unknown): AgEvent[];
27
+ //# sourceMappingURL=blocks.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"blocks.d.ts","sourceRoot":"","sources":["../src/blocks.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,OAAO,EAA6B,KAAK,OAAO,EAAE,MAAM,sBAAsB,CAAC;AAE/E,kFAAkF;AAClF,wBAAgB,kBAAkB,CAAC,GAAG,EAAE,OAAO,GAAG,OAAO,EAAE,CAK1D"}
package/dist/blocks.js ADDED
@@ -0,0 +1,32 @@
1
+ /**
2
+ * Frame → AgEvent[] ingestion for the opt-in block-preserving transcript.
3
+ *
4
+ * The wire carries ONE `message` frame per SSE event, but the *payload* shape
5
+ * differs by producer:
6
+ *
7
+ * - the deployed pod (`nocode-runtime`) emits a SINGLE AgEvent JSON OBJECT per
8
+ * frame (`sendEvent(res, 'message', e)`);
9
+ * - the CLI dev server (`guuey dev --serve`) batches an AgEvent[] ARRAY per
10
+ * frame (`sendEvent(res, 'message', batch)`).
11
+ *
12
+ * `ingestMessageFrame` is tolerant of BOTH shapes (object OR array). Validation
13
+ * is delegated to `@silverprotocol/core`'s `ingestAgEvents`, the library's own
14
+ * parse-known-else-skip consumer validator: any element that is not a valid
15
+ * AgJSON event — including bypass-mode SDKMessage shapes (`type: "assistant"` /
16
+ * `type: "result"`, which are NOT AgEvent `type` literals) — is dropped rather
17
+ * than crashing the stream or forcing a type lie. A frame that is not even a
18
+ * JSON value (`undefined`, a function, `NaN`, …) yields `[]`.
19
+ *
20
+ * The `unknown` → `JsonValue` narrowing is done at runtime via the exported
21
+ * `JsonValue` schema's `safeParse` (the same posture the Claude facet uses for
22
+ * provider-raw parts) — no assertion, no `as`.
23
+ */
24
+ import { ingestAgEvents, JsonValue } from "@silverprotocol/core";
25
+ /** Ingest one `message` SSE payload (object OR array) into validated AgEvents. */
26
+ export function ingestMessageFrame(raw) {
27
+ const parsed = JsonValue.safeParse(raw);
28
+ if (!parsed.success)
29
+ return [];
30
+ const value = parsed.data;
31
+ return ingestAgEvents(Array.isArray(value) ? value : [value]);
32
+ }
@@ -0,0 +1,62 @@
1
+ /**
2
+ * Shared transcript-history reader for the base-platform chat client.
3
+ *
4
+ * Reads a thread's persisted transcript from the public read plane
5
+ * (`GET {baseUrl}/threads/{id}/messages`, paginated by `nextToken`) so a
6
+ * reload can repaint history before any SSE traffic starts. Host-agnostic:
7
+ * the caller supplies the base URL and a `requestInit` carrying whatever
8
+ * identity that host can present (a `Authorization: Bearer` header on web /
9
+ * RN when signed in, an `x-guuey-guest` header for RN guests, cookies via
10
+ * `credentials: "include"`). Consumed by {@link createWebAdapters}; Portal
11
+ * has its own copy today and can migrate onto this later.
12
+ */
13
+ import type { JsonValue } from "@silverprotocol/core";
14
+ import type { AgentMessage, HistoryCard, HistoryLoadResult } from "./types";
15
+ /** One row of `GET /v1/threads/:id/messages`. */
16
+ export interface ThreadHistoryRow {
17
+ seq: number;
18
+ at: string;
19
+ kind: string;
20
+ authorRole: string;
21
+ text: string | null;
22
+ /**
23
+ * The verbatim persisted `AgArtifact` on `kind === "card"` rows; `null` or
24
+ * absent on text/event rows (the read plane omits it there). Forwarded
25
+ * opaquely — never re-parsed into AgEvents.
26
+ */
27
+ cardSnapshot?: JsonValue | null;
28
+ }
29
+ /** Project raw rows to chat turns: text rows only, author → role. */
30
+ export declare function threadHistoryRowsToMessages(rows: ThreadHistoryRow[]): AgentMessage[];
31
+ /**
32
+ * Project raw rows to persisted generative-UI cards: `kind === "card"` rows
33
+ * that actually carry a snapshot, tagged with their transcript position. The
34
+ * additive counterpart to {@link threadHistoryRowsToMessages} — a
35
+ * block-preserving consumer merges both by `seq`.
36
+ */
37
+ export declare function threadHistoryRowsToCards(rows: ThreadHistoryRow[]): HistoryCard[];
38
+ export interface ThreadHistoryFetchOptions {
39
+ /** Public read-plane base, already ending in `/v1`. */
40
+ baseUrl: string;
41
+ threadId: string;
42
+ /** Per-request init merged into each page fetch (headers, credentials). */
43
+ requestInit?: RequestInit;
44
+ /**
45
+ * Opt-in: ALSO project `kind === "card"` rows into `result.cards` (see
46
+ * {@link HistoryLoadResult}). Off by default so text-only consumers get the
47
+ * byte-identical `{ messages }` shape. On, a block-preserving renderer gets
48
+ * the persisted cards to interleave by `seq`.
49
+ */
50
+ includeCards?: boolean;
51
+ /** Injection seam for tests; defaults to the global `fetch`. */
52
+ fetchImpl?: typeof fetch;
53
+ }
54
+ /**
55
+ * Fetch a thread's transcript across all pages. Returns `{ gone: true }` on
56
+ * 403/404 (a stale local threadId the caller no longer owns / that no longer
57
+ * exists) so the hook can drop the persisted id; throws on any other non-OK
58
+ * status so `useAgentInvoke`'s best-effort caller can swallow it and leave
59
+ * the chat empty.
60
+ */
61
+ export declare function fetchThreadHistory({ baseUrl, threadId, requestInit, includeCards, fetchImpl, }: ThreadHistoryFetchOptions): Promise<HistoryLoadResult>;
62
+ //# sourceMappingURL=history.d.ts.map
@@ -0,0 +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;AAoBD,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,CAsBxD"}
@@ -0,0 +1,66 @@
1
+ /** Rows requested per history page. */
2
+ const HISTORY_PAGE_LIMIT = 100;
3
+ /**
4
+ * Hard bound on `nextToken` pagination: 10 pages × 100 rows = 1000 messages.
5
+ * The server pages in ASCENDING seq order and the newest turns arrive on the
6
+ * LAST pages, so we follow `nextToken` to completion within this cap rather
7
+ * than stopping at page 1 (which would drop exactly the turns a resuming user
8
+ * cares about). A >1000-message thread truncates its tail — accepted here;
9
+ * a server-side `sort=desc`/`from`-seq param would be the fix if it matters.
10
+ */
11
+ const MAX_HISTORY_PAGES = 10;
12
+ /** Project raw rows to chat turns: text rows only, author → role. */
13
+ export function threadHistoryRowsToMessages(rows) {
14
+ const messages = [];
15
+ for (const row of rows) {
16
+ if (row.kind !== "text" || row.text == null)
17
+ continue;
18
+ messages.push({ role: row.authorRole === "user" ? "user" : "assistant", text: row.text });
19
+ }
20
+ return messages;
21
+ }
22
+ /**
23
+ * Project raw rows to persisted generative-UI cards: `kind === "card"` rows
24
+ * that actually carry a snapshot, tagged with their transcript position. The
25
+ * additive counterpart to {@link threadHistoryRowsToMessages} — a
26
+ * block-preserving consumer merges both by `seq`.
27
+ */
28
+ export function threadHistoryRowsToCards(rows) {
29
+ const cards = [];
30
+ for (const row of rows) {
31
+ if (row.kind !== "card" || row.cardSnapshot == null)
32
+ continue;
33
+ cards.push({ seq: row.seq, at: row.at, cardSnapshot: row.cardSnapshot });
34
+ }
35
+ return cards;
36
+ }
37
+ /**
38
+ * Fetch a thread's transcript across all pages. Returns `{ gone: true }` on
39
+ * 403/404 (a stale local threadId the caller no longer owns / that no longer
40
+ * exists) so the hook can drop the persisted id; throws on any other non-OK
41
+ * status so `useAgentInvoke`'s best-effort caller can swallow it and leave
42
+ * the chat empty.
43
+ */
44
+ export async function fetchThreadHistory({ baseUrl, threadId, requestInit, includeCards = false, fetchImpl = fetch, }) {
45
+ const routeUrl = `${baseUrl}/threads/${encodeURIComponent(threadId)}/messages`;
46
+ const rows = [];
47
+ let nextToken = null;
48
+ for (let page = 0; page < MAX_HISTORY_PAGES; page++) {
49
+ const url = `${routeUrl}?limit=${HISTORY_PAGE_LIMIT}` +
50
+ (nextToken ? `&nextToken=${encodeURIComponent(nextToken)}` : "");
51
+ const res = await fetchImpl(url, requestInit);
52
+ if (res.status === 403 || res.status === 404)
53
+ return { gone: true };
54
+ if (!res.ok)
55
+ throw new Error(`history load failed: ${res.status}`);
56
+ const body = await res.json();
57
+ rows.push(...body.rows);
58
+ nextToken = body.nextToken;
59
+ if (!nextToken)
60
+ break;
61
+ }
62
+ return {
63
+ messages: threadHistoryRowsToMessages(rows),
64
+ ...(includeCards ? { cards: threadHistoryRowsToCards(rows) } : {}),
65
+ };
66
+ }
@@ -0,0 +1,8 @@
1
+ export { parseSseEvents, extractAssistantText, reduceAssistantText, stringField, type ParsedSseEvent, } from "./sse";
2
+ export { createWebAdapters, localStorageThreadStore, webGenerateId, fetchStreamTransport, AgentResponseError, type CreateWebAdaptersOptions, } from "./web-adapters";
3
+ export { fetchThreadHistory, threadHistoryRowsToMessages, threadHistoryRowsToCards, type ThreadHistoryRow, type ThreadHistoryFetchOptions, } from "./history";
4
+ export { ingestMessageFrame } from "./blocks";
5
+ export { asResourcePayload, asUiResource, blockUiResource, cardUiResource, isJsonObject, resourceHtml, scanProviderRawForUiResource, sortHistoryCards, toolNameFor, toolResultUiResource, type McpUiResourcePayload, } from "./block-ui";
6
+ export type { AgEvent, AgReduceResult, AgMessage, AgBlock } from "@silverprotocol/core";
7
+ export type { AgentMessage, HistoryCard, ThreadIdStore, GenerateId, InvokeRequest, InvokeTransport, AgentInvokeAdapters, AgentInvokeHistoryAdapter, HistoryLoadResult, UseAgentInvokeOptions, UseAgentInvokeReturn, } from "./types";
8
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +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,KAAK,cAAc,GACpB,MAAM,OAAO,CAAC;AACf,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,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,YAAY,EAAE,OAAO,EAAE,cAAc,EAAE,SAAS,EAAE,OAAO,EAAE,MAAM,sBAAsB,CAAC;AACxF,YAAY,EACV,YAAY,EACZ,WAAW,EACX,aAAa,EACb,UAAU,EACV,aAAa,EACb,eAAe,EACf,mBAAmB,EACnB,yBAAyB,EACzB,iBAAiB,EACjB,qBAAqB,EACrB,oBAAoB,GACrB,MAAM,SAAS,CAAC"}
package/dist/index.js ADDED
@@ -0,0 +1,7 @@
1
+ export { parseSseEvents, extractAssistantText, reduceAssistantText, stringField, } from "./sse";
2
+ export { createWebAdapters, localStorageThreadStore, webGenerateId, fetchStreamTransport, AgentResponseError, } from "./web-adapters";
3
+ export { fetchThreadHistory, threadHistoryRowsToMessages, threadHistoryRowsToCards, } from "./history";
4
+ export { ingestMessageFrame } from "./blocks";
5
+ // Pure block-walk / resource-narrowing helpers for a block-preserving renderer
6
+ // (shared by Studio's `AgentBlocks` and Portal-web's agent chat). React-free.
7
+ export { asResourcePayload, asUiResource, blockUiResource, cardUiResource, isJsonObject, resourceHtml, scanProviderRawForUiResource, sortHistoryCards, toolNameFor, toolResultUiResource, } from "./block-ui";
@@ -0,0 +1,11 @@
1
+ /**
2
+ * React entry point (`@guuey/agent-client/react`).
3
+ *
4
+ * The `useAgentInvoke` hook is the only React-coupled surface — the root
5
+ * subpath (`@guuey/agent-client`) stays React-free (pure SSE helpers, the
6
+ * history reader, and the web adapters). Consumers that only need those never
7
+ * import React at all; consumers that render chat import the hook from here.
8
+ */
9
+ export { useAgentInvoke, applyHistoryResult, type HistoryApplication } from "./useAgentInvoke";
10
+ export type { AgEvent, AgReduceResult } from "@silverprotocol/core";
11
+ //# sourceMappingURL=react.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"react.d.ts","sourceRoot":"","sources":["../src/react.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AACH,OAAO,EAAE,cAAc,EAAE,kBAAkB,EAAE,KAAK,kBAAkB,EAAE,MAAM,kBAAkB,CAAC;AAI/F,YAAY,EAAE,OAAO,EAAE,cAAc,EAAE,MAAM,sBAAsB,CAAC"}
package/dist/react.js ADDED
@@ -0,0 +1,9 @@
1
+ /**
2
+ * React entry point (`@guuey/agent-client/react`).
3
+ *
4
+ * The `useAgentInvoke` hook is the only React-coupled surface — the root
5
+ * subpath (`@guuey/agent-client`) stays React-free (pure SSE helpers, the
6
+ * history reader, and the web adapters). Consumers that only need those never
7
+ * import React at all; consumers that render chat import the hook from here.
8
+ */
9
+ export { useAgentInvoke, applyHistoryResult } from "./useAgentInvoke";