@guuey/agent-client 0.3.0 → 0.4.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/README.md +15 -2
- package/dist/error-codes.d.ts +58 -0
- package/dist/error-codes.d.ts.map +1 -0
- package/dist/error-codes.js +55 -0
- package/dist/errors.d.ts +39 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +36 -0
- package/dist/history.d.ts +1 -1
- package/dist/history.d.ts.map +1 -1
- package/dist/index.d.ts +10 -7
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +17 -6
- package/dist/react.d.ts +1 -1
- package/dist/react.d.ts.map +1 -1
- package/dist/react.js +1 -1
- package/dist/saturation-retry.d.ts +60 -0
- package/dist/saturation-retry.d.ts.map +1 -0
- package/dist/saturation-retry.js +135 -0
- package/dist/sse.d.ts +1 -1
- package/dist/sse.d.ts.map +1 -1
- package/dist/types.d.ts +29 -3
- package/dist/types.d.ts.map +1 -1
- package/dist/useAgentInvoke.d.ts +1 -1
- package/dist/useAgentInvoke.d.ts.map +1 -1
- package/dist/useAgentInvoke.js +23 -3
- package/dist/web-adapters.d.ts +43 -54
- package/dist/web-adapters.d.ts.map +1 -1
- package/dist/web-adapters.js +114 -27
- package/package.json +6 -5
- package/src/error-codes.ts +58 -0
- package/src/errors.ts +35 -0
- package/src/history.ts +1 -1
- package/src/index.ts +24 -9
- package/src/react.ts +1 -1
- package/src/saturation-retry.ts +150 -0
- package/src/sse.ts +1 -1
- package/src/types.ts +29 -3
- package/src/useAgentInvoke.ts +24 -4
- package/src/web-adapters.ts +144 -54
package/dist/useAgentInvoke.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* useAgentInvoke — the base-platform chat client.
|
|
3
3
|
*
|
|
4
4
|
* Speaks the nocode-runtime pod's Bedrock-style SSE contract (NOT the parked
|
|
5
|
-
* ggui generative-UI protocol that `@ggui-ai/react`'s useInvoke targets):
|
|
5
|
+
* ggui generative-UI protocol that `@ggui-ai/mcp-apps-react`'s useInvoke targets):
|
|
6
6
|
*
|
|
7
7
|
* POST {endpointUrl}/agent/invoke
|
|
8
8
|
* body: { input, threadId?, clientMessageId }
|
|
@@ -23,8 +23,9 @@
|
|
|
23
23
|
*/
|
|
24
24
|
import { useCallback, useEffect, useRef, useState } from "react";
|
|
25
25
|
import { Reducer } from "@silverprotocol/core";
|
|
26
|
-
import { parseConsentRequest, parseLinkRequest, parseSseEvents, reduceAssistantText, stringField, } from "./sse";
|
|
27
|
-
import { ingestMessageFrame } from "./blocks";
|
|
26
|
+
import { parseConsentRequest, parseLinkRequest, parseSseEvents, reduceAssistantText, stringField, } from "./sse.js";
|
|
27
|
+
import { ingestMessageFrame } from "./blocks.js";
|
|
28
|
+
import { AgentResponseError } from "./errors.js";
|
|
28
29
|
function threadStorageKey(appId) {
|
|
29
30
|
return `guuey:thread:${appId ?? "default"}`;
|
|
30
31
|
}
|
|
@@ -51,6 +52,10 @@ export function useAgentInvoke(opts) {
|
|
|
51
52
|
const [status, setStatus] = useState("ready");
|
|
52
53
|
const [activeTool, setActiveTool] = useState(null);
|
|
53
54
|
const [error, setError] = useState(null);
|
|
55
|
+
// The pod's wire code for whatever put `error` there, when the failure
|
|
56
|
+
// carried one (see the return-type contract). Moves in lockstep with
|
|
57
|
+
// `error` — every set/clear of one touches the other.
|
|
58
|
+
const [errorCode, setErrorCode] = useState(null);
|
|
54
59
|
const [threadId, setThreadId] = useState(null);
|
|
55
60
|
// Opt-in block-preserving transcript. `reduceResult` follows the
|
|
56
61
|
// null-until-first-valid-AgEvent contract documented on the return type: it
|
|
@@ -102,6 +107,7 @@ export function useAgentInvoke(opts) {
|
|
|
102
107
|
setThreadId(null);
|
|
103
108
|
setMessages([]);
|
|
104
109
|
setError(null);
|
|
110
|
+
setErrorCode(null);
|
|
105
111
|
setStatus("ready");
|
|
106
112
|
setActiveTool(null);
|
|
107
113
|
// Fresh conversation → drop the old fold; the reducer is rebuilt lazily on
|
|
@@ -192,6 +198,7 @@ export function useAgentInvoke(opts) {
|
|
|
192
198
|
void adaptersRef.current.storage.save(threadStorageKey(appId), "");
|
|
193
199
|
setMessages([]);
|
|
194
200
|
setError(null);
|
|
201
|
+
setErrorCode(null);
|
|
195
202
|
setStatus("ready");
|
|
196
203
|
setActiveTool(null);
|
|
197
204
|
// Re-create the reducer for the new conversation (rebuilt lazily on the
|
|
@@ -212,6 +219,7 @@ export function useAgentInvoke(opts) {
|
|
|
212
219
|
if (!endpointUrl || !input.trim() || status !== "ready")
|
|
213
220
|
return;
|
|
214
221
|
setError(null);
|
|
222
|
+
setErrorCode(null);
|
|
215
223
|
setStatus("connecting");
|
|
216
224
|
setMessages((prev) => [...prev, { role: "user", text: input }, { role: "assistant", text: "" }]);
|
|
217
225
|
const controller = new AbortController();
|
|
@@ -305,7 +313,12 @@ export function useAgentInvoke(opts) {
|
|
|
305
313
|
}
|
|
306
314
|
}
|
|
307
315
|
else if (ev.event === "error") {
|
|
316
|
+
// In-band failure frame — one of the two channels that carry the
|
|
317
|
+
// pod's wire code (the other is the pre-stream refusal caught
|
|
318
|
+
// below). A frame without a `code` clears it rather than leaving
|
|
319
|
+
// a previous turn's code standing beside a new message.
|
|
308
320
|
setError(stringField(ev.data, "message") ?? "agent error");
|
|
321
|
+
setErrorCode(stringField(ev.data, "code") ?? null);
|
|
309
322
|
}
|
|
310
323
|
else if (ev.event === "profile-consent-needed") {
|
|
311
324
|
// Cross-app profile consent ask (T6). Only a well-formed payload
|
|
@@ -331,6 +344,12 @@ export function useAgentInvoke(opts) {
|
|
|
331
344
|
catch (e) {
|
|
332
345
|
if (!controller.signal.aborted) {
|
|
333
346
|
setError(e instanceof Error ? e.message : "failed to reach agent");
|
|
347
|
+
// Pre-stream refusals arrive as a thrown AgentResponseError carrying
|
|
348
|
+
// the pod's structured code (a transport-level saturation retry has
|
|
349
|
+
// already happened and failed by the time one surfaces here). Any
|
|
350
|
+
// other throw — a network drop, a host-adapter failure — has no wire
|
|
351
|
+
// code, so the field stays null beside the message.
|
|
352
|
+
setErrorCode(e instanceof AgentResponseError ? (e.code ?? null) : null);
|
|
334
353
|
}
|
|
335
354
|
}
|
|
336
355
|
finally {
|
|
@@ -356,6 +375,7 @@ export function useAgentInvoke(opts) {
|
|
|
356
375
|
status,
|
|
357
376
|
activeTool,
|
|
358
377
|
error,
|
|
378
|
+
errorCode,
|
|
359
379
|
threadId,
|
|
360
380
|
abort,
|
|
361
381
|
reset,
|
package/dist/web-adapters.d.ts
CHANGED
|
@@ -6,43 +6,22 @@
|
|
|
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
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
* SSE stream opens). Carries the pod's structured `{ code, message }` when
|
|
13
|
-
* present — e.g. a `QUOTA_EXCEEDED` 429 whose message ("…reached its plan
|
|
14
|
-
* generation limit…") the chat UI should surface — falling back to the bare
|
|
15
|
-
* status for non-JSON failures.
|
|
16
|
-
*/
|
|
17
|
-
export declare class AgentResponseError extends Error {
|
|
18
|
-
readonly status: number;
|
|
19
|
-
readonly code?: string | undefined;
|
|
20
|
-
constructor(message: string, status: number, code?: string | undefined);
|
|
21
|
-
}
|
|
9
|
+
import { type McpToolCallResult, type ResolvedViewMount, type UiActionRequest } from "@guuey/mcp-apps-host";
|
|
10
|
+
import type { AgentInvokeAdapters, InvokeRequest, ThreadIdStore } from "./types.js";
|
|
11
|
+
import { type SaturationRetryOptions } from "./saturation-retry.js";
|
|
22
12
|
/** Persists the threadId in `window.localStorage` (synchronously). */
|
|
23
13
|
export declare const localStorageThreadStore: ThreadIdStore;
|
|
24
14
|
/** Crypto-strong client-message id, with a non-crypto fallback. */
|
|
25
15
|
export declare function webGenerateId(): string;
|
|
26
16
|
/**
|
|
27
|
-
*
|
|
28
|
-
*
|
|
29
|
-
*
|
|
30
|
-
*
|
|
31
|
-
*
|
|
32
|
-
*
|
|
33
|
-
* persists its own anonymous secret. The path for hosts with no usable
|
|
34
|
-
* cookie jar: React-Native, and the embedded widget, whose third-party
|
|
35
|
-
* iframe cannot rely on the pod's cookie surviving browser partitioning.
|
|
36
|
-
* The pod never mints a cookie for a header client.
|
|
37
|
-
* 3. neither → `credentials: "include"`, which round-trips the HttpOnly
|
|
38
|
-
* `guuey_guest` cookie the pod mints for anonymous browser callers.
|
|
39
|
-
*
|
|
40
|
-
* Never two at once: a bearer wins over a guest secret, and a request that
|
|
41
|
-
* carries either header does NOT also send cookie credentials.
|
|
42
|
-
*
|
|
43
|
-
* Reads the body via `ReadableStream.getReader()` (browser).
|
|
17
|
+
* The web SSE transport: {@link streamInvokeOnce} under the shared
|
|
18
|
+
* {@link withSaturationRetry} wrapper. Every consumer of this transport
|
|
19
|
+
* (Studio, the widget, anything built on {@link createWebAdapters}) therefore
|
|
20
|
+
* inherits the single `POD_SATURATED` retry, and inherits the SAME one Portal's
|
|
21
|
+
* React-Native transport wears — see that wrapper's docblock for which refusals
|
|
22
|
+
* retry, which deliberately do not, and why the retry is invisible to the hook.
|
|
44
23
|
*/
|
|
45
|
-
export declare function fetchStreamTransport(req: InvokeRequest, accessToken?: string | null, guestSecret?: string | null):
|
|
24
|
+
export declare function fetchStreamTransport(req: InvokeRequest, accessToken?: string | null, guestSecret?: string | null, options?: SaturationRetryOptions): AsyncIterable<string>;
|
|
46
25
|
export interface CreateWebAdaptersOptions {
|
|
47
26
|
/**
|
|
48
27
|
* Public read-plane base (ending in `/v1`) for transcript history. When
|
|
@@ -136,36 +115,46 @@ export interface CreateUiResourceReaderOptions {
|
|
|
136
115
|
fetchImpl?: typeof fetch;
|
|
137
116
|
}
|
|
138
117
|
/**
|
|
139
|
-
* Build a
|
|
140
|
-
*
|
|
141
|
-
* `GET /v1/threads/:threadId/ui-resource?uri=…`).
|
|
118
|
+
* Build a `UiResourceReader` over guuey's authenticated resources/read proxy
|
|
119
|
+
* (guuey#122 Gap 1: `GET /v1/threads/:threadId/ui-resource?uri=…`).
|
|
142
120
|
*
|
|
121
|
+
* This is `@guuey/mcp-apps-host`'s `createMcpUiResourceReader` assembly over
|
|
122
|
+
* a guuey-platform transport (guuey#127) — channel resolution and payload
|
|
123
|
+
* narrowing live in the host package; only the transport is guuey-shaped.
|
|
143
124
|
* The proxy owns EVERYTHING trust-shaped: caller identity (the same three
|
|
144
125
|
* families as the history read), thread ownership, the locator-to-thread
|
|
145
|
-
* scope guard, and the per-user federation mint. This
|
|
126
|
+
* scope guard, and the per-user federation mint. This transport only carries
|
|
146
127
|
* the surface's existing credential and maps EVERY non-OK — 401/403/404/502
|
|
147
128
|
* alike — to `undefined`: deny is byte-identical to a miss, and a miss
|
|
148
129
|
* renders the host's placeholder, never an error surface.
|
|
149
|
-
*
|
|
150
|
-
* Channel resolution is the phase-1 heuristic: a `ui://ggui/…` uri mounts in
|
|
151
|
-
* the ggui-CSP sandbox page (`channel: "ggui"`), anything else in the
|
|
152
|
-
* self-only page (`channel: "inline"`). Phase 2 (per-resource declared-CSP
|
|
153
|
-
* construction from the response `_meta.ui.csp`) retires the heuristic —
|
|
154
|
-
* conformance map, retirement step 3.
|
|
155
130
|
*/
|
|
131
|
+
export declare function createUiResourceReader(options: CreateUiResourceReaderOptions): (resourceUri: string) => Promise<ResolvedViewMount | undefined>;
|
|
132
|
+
/** Options for {@link createUiActionRelay} — same credential surface as the reader. */
|
|
133
|
+
export interface CreateUiActionRelayOptions {
|
|
134
|
+
/** The guuey public API base (`…/v1`). */
|
|
135
|
+
apiBaseUrl: string;
|
|
136
|
+
/** The thread whose persisted cards this relay may act for. */
|
|
137
|
+
threadId: string;
|
|
138
|
+
/** Signed-in bearer — wins over the guest secret (same rule as the transport). */
|
|
139
|
+
getAccessToken?: (opts?: {
|
|
140
|
+
forceRefresh?: boolean;
|
|
141
|
+
}) => Promise<string | null>;
|
|
142
|
+
/** Caller-owned anonymous guest secret (widget / guest chat). */
|
|
143
|
+
guestSecret?: string | null;
|
|
144
|
+
/** Injectable for tests. */
|
|
145
|
+
fetchImpl?: typeof fetch;
|
|
146
|
+
}
|
|
156
147
|
/**
|
|
157
|
-
*
|
|
158
|
-
*
|
|
159
|
-
*
|
|
160
|
-
*
|
|
148
|
+
* Build the card action relay over guuey's authenticated `tools/call` proxy
|
|
149
|
+
* (guuey#158: `POST /v1/threads/:threadId/ui-action`) — the mirror of
|
|
150
|
+
* {@link createUiResourceReader}. Allowlisting, arm narrowing, and the
|
|
151
|
+
* never-reject contract live in `@guuey/mcp-apps-host`'s
|
|
152
|
+
* `createMcpUiActionRelay`; only the transport is guuey-shaped. The proxy
|
|
153
|
+
* owns EVERYTHING trust-shaped (identity, thread ownership, the
|
|
154
|
+
* locator-to-thread guard, its own server-side allowlist, the per-user
|
|
155
|
+
* federation mint) — and every non-OK here collapses to `undefined`, which
|
|
156
|
+
* the host relay answers in-band as an `isError` result, never a thrown
|
|
157
|
+
* error into the sandbox bridge.
|
|
161
158
|
*/
|
|
162
|
-
export
|
|
163
|
-
channel: "inline" | "ggui";
|
|
164
|
-
resource: {
|
|
165
|
-
uri: string;
|
|
166
|
-
mimeType?: string;
|
|
167
|
-
text: string;
|
|
168
|
-
};
|
|
169
|
-
}
|
|
170
|
-
export declare function createUiResourceReader(options: CreateUiResourceReaderOptions): (resourceUri: string) => Promise<ResolvedUiResourceMount | undefined>;
|
|
159
|
+
export declare function createUiActionRelay(options: CreateUiActionRelayOptions): (request: UiActionRequest) => Promise<McpToolCallResult>;
|
|
171
160
|
//# 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,
|
|
1
|
+
{"version":3,"file":"web-adapters.d.ts","sourceRoot":"","sources":["../src/web-adapters.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AACH,OAAO,EAIL,KAAK,iBAAiB,EAEtB,KAAK,iBAAiB,EACtB,KAAK,eAAe,EACrB,MAAM,sBAAsB,CAAC;AAC9B,OAAO,KAAK,EACV,mBAAmB,EACnB,aAAa,EAEb,aAAa,EACd,MAAM,YAAY,CAAC;AAGpB,OAAO,EAGL,KAAK,sBAAsB,EAC5B,MAAM,uBAAuB,CAAC;AAE/B,sEAAsE;AACtE,eAAO,MAAM,uBAAuB,EAAE,aAiBrC,CAAC;AAEF,mEAAmE;AACnE,wBAAgB,aAAa,IAAI,MAAM,CAKtC;AAsHD;;;;;;;GAOG;AACH,wBAAgB,oBAAoB,CAClC,GAAG,EAAE,aAAa,EAClB,WAAW,CAAC,EAAE,MAAM,GAAG,IAAI,EAC3B,WAAW,CAAC,EAAE,MAAM,GAAG,IAAI,EAC3B,OAAO,GAAE,sBAA2B,GACnC,aAAa,CAAC,MAAM,CAAC,CAKvB;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;AAED,uFAAuF;AACvF,MAAM,WAAW,0BAA0B;IACzC,0CAA0C;IAC1C,UAAU,EAAE,MAAM,CAAC;IACnB,+DAA+D;IAC/D,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;;;;;;;;;;;GAWG;AACH,wBAAgB,mBAAmB,CACjC,OAAO,EAAE,0BAA0B,GAClC,CAAC,OAAO,EAAE,eAAe,KAAK,OAAO,CAAC,iBAAiB,CAAC,CA+C1D"}
|
package/dist/web-adapters.js
CHANGED
|
@@ -1,21 +1,15 @@
|
|
|
1
|
-
import { fetchThreadHistory, HistoryUnauthorizedError } from "./history";
|
|
2
1
|
/**
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
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
8
|
*/
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
super(message);
|
|
14
|
-
this.status = status;
|
|
15
|
-
this.code = code;
|
|
16
|
-
this.name = "AgentResponseError";
|
|
17
|
-
}
|
|
18
|
-
}
|
|
9
|
+
import { createMcpUiActionRelay, createMcpUiResourceReader, } from "@guuey/mcp-apps-host";
|
|
10
|
+
import { fetchThreadHistory, HistoryUnauthorizedError } from "./history.js";
|
|
11
|
+
import { AgentResponseError } from "./errors.js";
|
|
12
|
+
import { parseRetryAfterSeconds, withSaturationRetry, } from "./saturation-retry.js";
|
|
19
13
|
/** Persists the threadId in `window.localStorage` (synchronously). */
|
|
20
14
|
export const localStorageThreadStore = {
|
|
21
15
|
load(key) {
|
|
@@ -82,7 +76,11 @@ function sendableGuestSecret(secret) {
|
|
|
82
76
|
return typeof secret === "string" && GUEST_SECRET_RE.test(secret) ? secret : null;
|
|
83
77
|
}
|
|
84
78
|
/**
|
|
85
|
-
*
|
|
79
|
+
* One invoke attempt: opens the request and yields decoded SSE chunks.
|
|
80
|
+
* {@link fetchStreamTransport} wraps this with the shared saturation retry —
|
|
81
|
+
* every behaviour below is per-attempt.
|
|
82
|
+
*
|
|
83
|
+
* Exactly ONE identity carrier per request, in order:
|
|
86
84
|
*
|
|
87
85
|
* 1. `accessToken` → `Authorization: Bearer` — the pod identifies the caller
|
|
88
86
|
* by their verified access token (the same identity the history read
|
|
@@ -100,7 +98,7 @@ function sendableGuestSecret(secret) {
|
|
|
100
98
|
*
|
|
101
99
|
* Reads the body via `ReadableStream.getReader()` (browser).
|
|
102
100
|
*/
|
|
103
|
-
|
|
101
|
+
async function* streamInvokeOnce(req, accessToken, guestSecret) {
|
|
104
102
|
const headers = {
|
|
105
103
|
"Content-Type": "application/json",
|
|
106
104
|
Accept: "text/event-stream",
|
|
@@ -137,7 +135,7 @@ export async function* fetchStreamTransport(req, accessToken, guestSecret) {
|
|
|
137
135
|
code = body.code;
|
|
138
136
|
}
|
|
139
137
|
}
|
|
140
|
-
throw new AgentResponseError(message, resp.status, code);
|
|
138
|
+
throw new AgentResponseError(message, resp.status, code, parseRetryAfterSeconds(resp.headers.get("Retry-After")));
|
|
141
139
|
}
|
|
142
140
|
const reader = resp.body.getReader();
|
|
143
141
|
const decoder = new TextDecoder();
|
|
@@ -148,6 +146,17 @@ export async function* fetchStreamTransport(req, accessToken, guestSecret) {
|
|
|
148
146
|
yield decoder.decode(value, { stream: true });
|
|
149
147
|
}
|
|
150
148
|
}
|
|
149
|
+
/**
|
|
150
|
+
* The web SSE transport: {@link streamInvokeOnce} under the shared
|
|
151
|
+
* {@link withSaturationRetry} wrapper. Every consumer of this transport
|
|
152
|
+
* (Studio, the widget, anything built on {@link createWebAdapters}) therefore
|
|
153
|
+
* inherits the single `POD_SATURATED` retry, and inherits the SAME one Portal's
|
|
154
|
+
* React-Native transport wears — see that wrapper's docblock for which refusals
|
|
155
|
+
* retry, which deliberately do not, and why the retry is invisible to the hook.
|
|
156
|
+
*/
|
|
157
|
+
export function fetchStreamTransport(req, accessToken, guestSecret, options = {}) {
|
|
158
|
+
return withSaturationRetry((attempt) => streamInvokeOnce(attempt, accessToken, guestSecret), options)(req);
|
|
159
|
+
}
|
|
151
160
|
/**
|
|
152
161
|
* Build the web host-adapter bundle for {@link useAgentInvoke}. Pass an
|
|
153
162
|
* access-token resolver and/or a guest-secret resolver (plus the read-plane
|
|
@@ -227,9 +236,23 @@ 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
|
+
*/
|
|
230
253
|
export function createUiResourceReader(options) {
|
|
231
254
|
const fetchImpl = options.fetchImpl ?? fetch;
|
|
232
|
-
|
|
255
|
+
const readResource = async (resourceUri) => {
|
|
233
256
|
const headers = {};
|
|
234
257
|
const token = options.getAccessToken ? await options.getAccessToken() : null;
|
|
235
258
|
const guest = sendableGuestSecret(options.guestSecret);
|
|
@@ -272,15 +295,79 @@ export function createUiResourceReader(options) {
|
|
|
272
295
|
catch {
|
|
273
296
|
return undefined;
|
|
274
297
|
}
|
|
275
|
-
|
|
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")
|
|
276
303
|
return undefined;
|
|
277
304
|
return {
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
text: body.text,
|
|
283
|
-
},
|
|
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 } : {}),
|
|
284
309
|
};
|
|
285
310
|
};
|
|
311
|
+
return createMcpUiResourceReader({ readResource });
|
|
312
|
+
}
|
|
313
|
+
/**
|
|
314
|
+
* Build the card action relay over guuey's authenticated `tools/call` proxy
|
|
315
|
+
* (guuey#158: `POST /v1/threads/:threadId/ui-action`) — the mirror of
|
|
316
|
+
* {@link createUiResourceReader}. Allowlisting, arm narrowing, and the
|
|
317
|
+
* never-reject contract live in `@guuey/mcp-apps-host`'s
|
|
318
|
+
* `createMcpUiActionRelay`; only the transport is guuey-shaped. The proxy
|
|
319
|
+
* owns EVERYTHING trust-shaped (identity, thread ownership, the
|
|
320
|
+
* locator-to-thread guard, its own server-side allowlist, the per-user
|
|
321
|
+
* federation mint) — and every non-OK here collapses to `undefined`, which
|
|
322
|
+
* the host relay answers in-band as an `isError` result, never a thrown
|
|
323
|
+
* error into the sandbox bridge.
|
|
324
|
+
*/
|
|
325
|
+
export function createUiActionRelay(options) {
|
|
326
|
+
const fetchImpl = options.fetchImpl ?? fetch;
|
|
327
|
+
const callTool = async (uri, name, args) => {
|
|
328
|
+
const headers = { "content-type": "application/json" };
|
|
329
|
+
const token = options.getAccessToken ? await options.getAccessToken() : null;
|
|
330
|
+
const guest = sendableGuestSecret(options.guestSecret);
|
|
331
|
+
if (token) {
|
|
332
|
+
headers["authorization"] = `Bearer ${token}`;
|
|
333
|
+
}
|
|
334
|
+
else if (guest) {
|
|
335
|
+
headers[GUEST_HEADER] = guest;
|
|
336
|
+
}
|
|
337
|
+
const requestUrl = `${options.apiBaseUrl}/threads/${encodeURIComponent(options.threadId)}/ui-action`;
|
|
338
|
+
const body = JSON.stringify({ uri, name, ...(args !== undefined ? { arguments: args } : {}) });
|
|
339
|
+
let res;
|
|
340
|
+
try {
|
|
341
|
+
res = await fetchImpl(requestUrl, { method: "POST", headers, body });
|
|
342
|
+
}
|
|
343
|
+
catch {
|
|
344
|
+
return undefined; // transport failure — the host relay answers in-band
|
|
345
|
+
}
|
|
346
|
+
// One forceRefresh retry on 401 with a bearer in play — the same
|
|
347
|
+
// expired-but-refreshable recovery the reader performs.
|
|
348
|
+
if (res.status === 401 && options.getAccessToken) {
|
|
349
|
+
const fresh = await options.getAccessToken({ forceRefresh: true }).catch(() => null);
|
|
350
|
+
if (fresh) {
|
|
351
|
+
try {
|
|
352
|
+
res = await fetchImpl(requestUrl, {
|
|
353
|
+
method: "POST",
|
|
354
|
+
headers: { ...headers, authorization: `Bearer ${fresh}` },
|
|
355
|
+
body,
|
|
356
|
+
});
|
|
357
|
+
}
|
|
358
|
+
catch {
|
|
359
|
+
return undefined;
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
if (!res.ok)
|
|
364
|
+
return undefined;
|
|
365
|
+
try {
|
|
366
|
+
return (await res.json());
|
|
367
|
+
}
|
|
368
|
+
catch {
|
|
369
|
+
return undefined;
|
|
370
|
+
}
|
|
371
|
+
};
|
|
372
|
+
return createMcpUiActionRelay({ callTool });
|
|
286
373
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@guuey/agent-client",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
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,7 +28,8 @@
|
|
|
28
28
|
}
|
|
29
29
|
},
|
|
30
30
|
"dependencies": {
|
|
31
|
-
"@silverprotocol/core": "0.4.1"
|
|
31
|
+
"@silverprotocol/core": "0.4.1",
|
|
32
|
+
"@guuey/mcp-apps-host": "0.4.0"
|
|
32
33
|
},
|
|
33
34
|
"peerDependencies": {
|
|
34
35
|
"react": ">=18"
|
|
@@ -55,14 +56,14 @@
|
|
|
55
56
|
"client"
|
|
56
57
|
],
|
|
57
58
|
"homepage": "https://guuey.com",
|
|
58
|
-
"bugs": {
|
|
59
|
-
"url": "https://github.com/loqu-co/guuey/issues"
|
|
60
|
-
},
|
|
61
59
|
"repository": {
|
|
62
60
|
"type": "git",
|
|
63
61
|
"url": "git+https://github.com/withguuey/guuey-sdks.git",
|
|
64
62
|
"directory": "packages/agent-client"
|
|
65
63
|
},
|
|
64
|
+
"bugs": {
|
|
65
|
+
"url": "https://github.com/withguuey/guuey-sdks/issues"
|
|
66
|
+
},
|
|
66
67
|
"scripts": {
|
|
67
68
|
"build": "tsc -p tsconfig.build.json",
|
|
68
69
|
"dev": "tsc --watch",
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The pod's error-envelope wire codes, TRANSCRIBED.
|
|
3
|
+
*
|
|
4
|
+
* The source of truth is the runtime's own private module
|
|
5
|
+
* (`backend/services/nocode-runtime/src/error-codes.ts`). This package is
|
|
6
|
+
* published to npm and cannot take a `@guuey-private` dependency, so it keeps
|
|
7
|
+
* its own copy — the same arrangement as {@link GUEST_HEADER} in
|
|
8
|
+
* `./web-adapters.ts` and `@guuey/host`'s mirrored fs-contract constants. The
|
|
9
|
+
* copies are not trusted to prose alone: `agent-client-codes.sync.test.ts` in
|
|
10
|
+
* the runtime package (which can import BOTH) asserts they stay identical, so
|
|
11
|
+
* renaming a code on either side fails that test rather than silently breaking
|
|
12
|
+
* a client branch.
|
|
13
|
+
*
|
|
14
|
+
* WIRE CONTRACT (what these codes appear in):
|
|
15
|
+
*
|
|
16
|
+
* - a pre-stream refusal — `{ "code": …, "message": … }` with an HTTP status,
|
|
17
|
+
* parsed into {@link AgentResponseError};
|
|
18
|
+
* - an in-band failure — `event: error` / `data: { code, message }`, surfaced
|
|
19
|
+
* as `useAgentInvoke`'s `errorCode`.
|
|
20
|
+
*
|
|
21
|
+
* Both channels carry the SAME vocabulary, which is why one mirror serves them.
|
|
22
|
+
*/
|
|
23
|
+
export const AGENT_ERROR_CODES = {
|
|
24
|
+
/** No usable identity on a surface that requires one. */
|
|
25
|
+
UNAUTHORIZED: "UNAUTHORIZED",
|
|
26
|
+
/** The invoke body did not parse / validate. */
|
|
27
|
+
INVALID_REQUEST: "INVALID_REQUEST",
|
|
28
|
+
/** The builder turned anonymous access off for this agent. */
|
|
29
|
+
GUEST_ACCESS_DISABLED: "GUEST_ACCESS_DISABLED",
|
|
30
|
+
/** The caller (or the app) is out of plan allowance — the upgrade prompt. */
|
|
31
|
+
QUOTA_EXCEEDED: "QUOTA_EXCEEDED",
|
|
32
|
+
/** The app hit its builder-set managed spend cap. */
|
|
33
|
+
MANAGED_SPEND_CAP: "MANAGED_SPEND_CAP",
|
|
34
|
+
/**
|
|
35
|
+
* The pod is at its concurrent-turn cap (scaling S1-F3). A 503 carrying a
|
|
36
|
+
* `Retry-After` hint, and the ONE code {@link fetchStreamTransport} retries
|
|
37
|
+
* by itself — see its docblock for the single-attempt rule.
|
|
38
|
+
*/
|
|
39
|
+
POD_SATURATED: "POD_SATURATED",
|
|
40
|
+
/**
|
|
41
|
+
* The pod took SIGTERM and refuses NEW turns while in-flight ones finish.
|
|
42
|
+
* Also a 503 + `Retry-After`, but deliberately NOT auto-retried (the
|
|
43
|
+
* endpoint pull re-routes the next request; retrying into the same pod is
|
|
44
|
+
* the one thing guaranteed not to help).
|
|
45
|
+
*/
|
|
46
|
+
DRAINING: "DRAINING",
|
|
47
|
+
/** Refused for this caller — e.g. the link-prompt dismiss route's byo-only rule. */
|
|
48
|
+
FORBIDDEN: "FORBIDDEN",
|
|
49
|
+
/** The turn ran past the pod's wall-clock budget. */
|
|
50
|
+
TIMEOUT: "TIMEOUT",
|
|
51
|
+
/** A guuey-side dependency failed (not the agent's own code). */
|
|
52
|
+
PLATFORM_ERROR: "PLATFORM_ERROR",
|
|
53
|
+
/** Unclassified pod failure. */
|
|
54
|
+
INTERNAL: "INTERNAL",
|
|
55
|
+
} as const;
|
|
56
|
+
|
|
57
|
+
/** One of the pod's wire codes — see {@link AGENT_ERROR_CODES}. */
|
|
58
|
+
export type AgentErrorCode = (typeof AGENT_ERROR_CODES)[keyof typeof AGENT_ERROR_CODES];
|
package/src/errors.ts
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Error types the transports throw and the hook branches on.
|
|
3
|
+
*
|
|
4
|
+
* Its own module (rather than living in `./web-adapters.ts`) so `useAgentInvoke`
|
|
5
|
+
* — which must stay platform-agnostic — can `instanceof`-narrow a caught error
|
|
6
|
+
* without pulling the web adapter bundle (`fetch`, the history reader,
|
|
7
|
+
* `@guuey/mcp-apps-host`) into a React-Native build.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Thrown when the pod returns a non-2xx status on `/agent/invoke` (before any
|
|
12
|
+
* SSE stream opens). Carries the pod's structured `{ code, message }` when
|
|
13
|
+
* present — e.g. a `QUOTA_EXCEEDED` 429 whose message ("…reached its plan
|
|
14
|
+
* generation limit…") the chat UI should surface — falling back to the bare
|
|
15
|
+
* status for non-JSON failures. See `AGENT_ERROR_CODES` for the vocabulary.
|
|
16
|
+
*/
|
|
17
|
+
export class AgentResponseError extends Error {
|
|
18
|
+
constructor(
|
|
19
|
+
message: string,
|
|
20
|
+
readonly status: number,
|
|
21
|
+
readonly code?: string,
|
|
22
|
+
/**
|
|
23
|
+
* The response's `Retry-After` hint in whole seconds, when it sent a
|
|
24
|
+
* parseable one. The pod attaches it to its two 503 refusals
|
|
25
|
+
* (`POD_SATURATED`, `DRAINING`) and exposes the header across origins via
|
|
26
|
+
* `Access-Control-Expose-Headers`, so a browser client can actually read
|
|
27
|
+
* it. `undefined` when the header was absent, malformed, or in the
|
|
28
|
+
* HTTP-date form the pod never emits.
|
|
29
|
+
*/
|
|
30
|
+
readonly retryAfterSeconds?: number,
|
|
31
|
+
) {
|
|
32
|
+
super(message);
|
|
33
|
+
this.name = "AgentResponseError";
|
|
34
|
+
}
|
|
35
|
+
}
|
package/src/history.ts
CHANGED
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
* has its own copy today and can migrate onto this later.
|
|
12
12
|
*/
|
|
13
13
|
import type { AgMessage, JsonValue } from "@silverprotocol/core";
|
|
14
|
-
import type { AgentMessage, HistoryCard, HistoryLoadResult } from "./types";
|
|
14
|
+
import type { AgentMessage, HistoryCard, HistoryLoadResult } from "./types.js";
|
|
15
15
|
|
|
16
16
|
/** One row of `GET /v1/threads/:id/messages`. */
|
|
17
17
|
export interface ThreadHistoryRow {
|
package/src/index.ts
CHANGED
|
@@ -6,19 +6,34 @@ export {
|
|
|
6
6
|
parseConsentRequest,
|
|
7
7
|
parseLinkRequest,
|
|
8
8
|
type ParsedSseEvent,
|
|
9
|
-
} from "./sse";
|
|
10
|
-
export { dismissLinkPrompt } from "./link-prompt";
|
|
9
|
+
} from "./sse.js";
|
|
10
|
+
export { dismissLinkPrompt } from "./link-prompt.js";
|
|
11
11
|
export {
|
|
12
|
+
createUiActionRelay,
|
|
13
|
+
type CreateUiActionRelayOptions,
|
|
12
14
|
createUiResourceReader,
|
|
13
15
|
type CreateUiResourceReaderOptions,
|
|
14
|
-
type ResolvedUiResourceMount,
|
|
15
16
|
createWebAdapters,
|
|
16
17
|
localStorageThreadStore,
|
|
17
18
|
webGenerateId,
|
|
18
19
|
fetchStreamTransport,
|
|
19
|
-
AgentResponseError,
|
|
20
20
|
type CreateWebAdaptersOptions,
|
|
21
|
-
} from "./web-adapters";
|
|
21
|
+
} from "./web-adapters.js";
|
|
22
|
+
// The `POD_SATURATED` single-retry wrapper, transport-agnostic: a host that
|
|
23
|
+
// brings its own `fetch` (Portal's React-Native transport) wraps it to wear the
|
|
24
|
+
// same semantics as the web transport instead of hand-rolling a second copy.
|
|
25
|
+
// `parseRetryAfterSeconds` ships with it because filling
|
|
26
|
+
// `AgentResponseError.retryAfterSeconds` the same way is what makes the wrapper
|
|
27
|
+
// honour the pod's hint.
|
|
28
|
+
export {
|
|
29
|
+
withSaturationRetry,
|
|
30
|
+
parseRetryAfterSeconds,
|
|
31
|
+
type SaturationRetryOptions,
|
|
32
|
+
} from "./saturation-retry.js";
|
|
33
|
+
export { AgentResponseError } from "./errors.js";
|
|
34
|
+
// The pod's wire-code vocabulary, mirrored — branch on these instead of
|
|
35
|
+
// re-typing the string literals (see the module docblock for the sync guard).
|
|
36
|
+
export { AGENT_ERROR_CODES, type AgentErrorCode } from "./error-codes.js";
|
|
22
37
|
export {
|
|
23
38
|
fetchThreadHistory,
|
|
24
39
|
threadHistoryRowsToMessages,
|
|
@@ -26,13 +41,13 @@ export {
|
|
|
26
41
|
HistoryUnauthorizedError,
|
|
27
42
|
type ThreadHistoryRow,
|
|
28
43
|
type ThreadHistoryFetchOptions,
|
|
29
|
-
} from "./history";
|
|
30
|
-
export { ingestMessageFrame } from "./blocks";
|
|
44
|
+
} from "./history.js";
|
|
45
|
+
export { ingestMessageFrame } from "./blocks.js";
|
|
31
46
|
// Pure block-walk / resource-narrowing helpers for a block-preserving renderer
|
|
32
47
|
// (shared by Studio's `AgentBlocks` and Portal-web's agent chat). React-free.
|
|
33
48
|
// Transcript labeling/ordering helpers (mount narrowing itself moved to
|
|
34
49
|
// @guuey/mcp-apps-host — the SEP-1865 Host role package; import it directly).
|
|
35
|
-
export { sortHistoryCards, toolNameFor } from "./history";
|
|
50
|
+
export { sortHistoryCards, toolNameFor } from "./history.js";
|
|
36
51
|
// Re-export the AgJSON types the block-preserving transcript surfaces, so
|
|
37
52
|
// consumers can name `reduceResult` / block types without a direct
|
|
38
53
|
// `@silverprotocol/core` import.
|
|
@@ -52,4 +67,4 @@ export type {
|
|
|
52
67
|
HistoryLoadResult,
|
|
53
68
|
UseAgentInvokeOptions,
|
|
54
69
|
UseAgentInvokeReturn,
|
|
55
|
-
} from "./types";
|
|
70
|
+
} from "./types.js";
|
package/src/react.ts
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
* history reader, and the web adapters). Consumers that only need those never
|
|
7
7
|
* import React at all; consumers that render chat import the hook from here.
|
|
8
8
|
*/
|
|
9
|
-
export { useAgentInvoke, applyHistoryResult, type HistoryApplication } from "./useAgentInvoke";
|
|
9
|
+
export { useAgentInvoke, applyHistoryResult, type HistoryApplication } from "./useAgentInvoke.js";
|
|
10
10
|
// The block-preserving transcript surfaces `AgReduceResult`; re-export it (and
|
|
11
11
|
// `AgEvent`) here so `./react` consumers can type `reduceResult` without a
|
|
12
12
|
// direct `@silverprotocol/core` import.
|