@guuey/agent-client 0.3.1 → 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 +38 -32
- package/dist/web-adapters.d.ts.map +1 -1
- package/dist/web-adapters.js +83 -22
- package/package.json +5 -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 -8
- 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 +121 -23
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,44 +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 ResolvedViewMount } from "@guuey/mcp-apps-host";
|
|
10
|
-
import type { AgentInvokeAdapters, InvokeRequest, ThreadIdStore } from "./types";
|
|
11
|
-
|
|
12
|
-
* Thrown when the pod returns a non-2xx status on `/agent/invoke` (before any
|
|
13
|
-
* SSE stream opens). Carries the pod's structured `{ code, message }` when
|
|
14
|
-
* present — e.g. a `QUOTA_EXCEEDED` 429 whose message ("…reached its plan
|
|
15
|
-
* generation limit…") the chat UI should surface — falling back to the bare
|
|
16
|
-
* status for non-JSON failures.
|
|
17
|
-
*/
|
|
18
|
-
export declare class AgentResponseError extends Error {
|
|
19
|
-
readonly status: number;
|
|
20
|
-
readonly code?: string | undefined;
|
|
21
|
-
constructor(message: string, status: number, code?: string | undefined);
|
|
22
|
-
}
|
|
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";
|
|
23
12
|
/** Persists the threadId in `window.localStorage` (synchronously). */
|
|
24
13
|
export declare const localStorageThreadStore: ThreadIdStore;
|
|
25
14
|
/** Crypto-strong client-message id, with a non-crypto fallback. */
|
|
26
15
|
export declare function webGenerateId(): string;
|
|
27
16
|
/**
|
|
28
|
-
*
|
|
29
|
-
*
|
|
30
|
-
*
|
|
31
|
-
*
|
|
32
|
-
*
|
|
33
|
-
*
|
|
34
|
-
* persists its own anonymous secret. The path for hosts with no usable
|
|
35
|
-
* cookie jar: React-Native, and the embedded widget, whose third-party
|
|
36
|
-
* iframe cannot rely on the pod's cookie surviving browser partitioning.
|
|
37
|
-
* The pod never mints a cookie for a header client.
|
|
38
|
-
* 3. neither → `credentials: "include"`, which round-trips the HttpOnly
|
|
39
|
-
* `guuey_guest` cookie the pod mints for anonymous browser callers.
|
|
40
|
-
*
|
|
41
|
-
* Never two at once: a bearer wins over a guest secret, and a request that
|
|
42
|
-
* carries either header does NOT also send cookie credentials.
|
|
43
|
-
*
|
|
44
|
-
* 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.
|
|
45
23
|
*/
|
|
46
|
-
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>;
|
|
47
25
|
export interface CreateWebAdaptersOptions {
|
|
48
26
|
/**
|
|
49
27
|
* Public read-plane base (ending in `/v1`) for transcript history. When
|
|
@@ -151,4 +129,32 @@ export interface CreateUiResourceReaderOptions {
|
|
|
151
129
|
* renders the host's placeholder, never an error surface.
|
|
152
130
|
*/
|
|
153
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
|
+
}
|
|
147
|
+
/**
|
|
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.
|
|
158
|
+
*/
|
|
159
|
+
export declare function createUiActionRelay(options: CreateUiActionRelayOptions): (request: UiActionRequest) => Promise<McpToolCallResult>;
|
|
154
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,
|
|
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
|
@@ -6,25 +6,10 @@
|
|
|
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 { createMcpUiResourceReader, } from "@guuey/mcp-apps-host";
|
|
10
|
-
import { fetchThreadHistory, HistoryUnauthorizedError } from "./history";
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
* SSE stream opens). Carries the pod's structured `{ code, message }` when
|
|
14
|
-
* present — e.g. a `QUOTA_EXCEEDED` 429 whose message ("…reached its plan
|
|
15
|
-
* generation limit…") the chat UI should surface — falling back to the bare
|
|
16
|
-
* status for non-JSON failures.
|
|
17
|
-
*/
|
|
18
|
-
export class AgentResponseError extends Error {
|
|
19
|
-
status;
|
|
20
|
-
code;
|
|
21
|
-
constructor(message, status, code) {
|
|
22
|
-
super(message);
|
|
23
|
-
this.status = status;
|
|
24
|
-
this.code = code;
|
|
25
|
-
this.name = "AgentResponseError";
|
|
26
|
-
}
|
|
27
|
-
}
|
|
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";
|
|
28
13
|
/** Persists the threadId in `window.localStorage` (synchronously). */
|
|
29
14
|
export const localStorageThreadStore = {
|
|
30
15
|
load(key) {
|
|
@@ -91,7 +76,11 @@ function sendableGuestSecret(secret) {
|
|
|
91
76
|
return typeof secret === "string" && GUEST_SECRET_RE.test(secret) ? secret : null;
|
|
92
77
|
}
|
|
93
78
|
/**
|
|
94
|
-
*
|
|
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:
|
|
95
84
|
*
|
|
96
85
|
* 1. `accessToken` → `Authorization: Bearer` — the pod identifies the caller
|
|
97
86
|
* by their verified access token (the same identity the history read
|
|
@@ -109,7 +98,7 @@ function sendableGuestSecret(secret) {
|
|
|
109
98
|
*
|
|
110
99
|
* Reads the body via `ReadableStream.getReader()` (browser).
|
|
111
100
|
*/
|
|
112
|
-
|
|
101
|
+
async function* streamInvokeOnce(req, accessToken, guestSecret) {
|
|
113
102
|
const headers = {
|
|
114
103
|
"Content-Type": "application/json",
|
|
115
104
|
Accept: "text/event-stream",
|
|
@@ -146,7 +135,7 @@ export async function* fetchStreamTransport(req, accessToken, guestSecret) {
|
|
|
146
135
|
code = body.code;
|
|
147
136
|
}
|
|
148
137
|
}
|
|
149
|
-
throw new AgentResponseError(message, resp.status, code);
|
|
138
|
+
throw new AgentResponseError(message, resp.status, code, parseRetryAfterSeconds(resp.headers.get("Retry-After")));
|
|
150
139
|
}
|
|
151
140
|
const reader = resp.body.getReader();
|
|
152
141
|
const decoder = new TextDecoder();
|
|
@@ -157,6 +146,17 @@ export async function* fetchStreamTransport(req, accessToken, guestSecret) {
|
|
|
157
146
|
yield decoder.decode(value, { stream: true });
|
|
158
147
|
}
|
|
159
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
|
+
}
|
|
160
160
|
/**
|
|
161
161
|
* Build the web host-adapter bundle for {@link useAgentInvoke}. Pass an
|
|
162
162
|
* access-token resolver and/or a guest-secret resolver (plus the read-plane
|
|
@@ -310,3 +310,64 @@ export function createUiResourceReader(options) {
|
|
|
310
310
|
};
|
|
311
311
|
return createMcpUiResourceReader({ readResource });
|
|
312
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 });
|
|
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",
|
|
@@ -29,7 +29,7 @@
|
|
|
29
29
|
},
|
|
30
30
|
"dependencies": {
|
|
31
31
|
"@silverprotocol/core": "0.4.1",
|
|
32
|
-
"@guuey/mcp-apps-host": "0.
|
|
32
|
+
"@guuey/mcp-apps-host": "0.4.0"
|
|
33
33
|
},
|
|
34
34
|
"peerDependencies": {
|
|
35
35
|
"react": ">=18"
|
|
@@ -56,14 +56,14 @@
|
|
|
56
56
|
"client"
|
|
57
57
|
],
|
|
58
58
|
"homepage": "https://guuey.com",
|
|
59
|
-
"bugs": {
|
|
60
|
-
"url": "https://github.com/loqu-co/guuey/issues"
|
|
61
|
-
},
|
|
62
59
|
"repository": {
|
|
63
60
|
"type": "git",
|
|
64
61
|
"url": "git+https://github.com/withguuey/guuey-sdks.git",
|
|
65
62
|
"directory": "packages/agent-client"
|
|
66
63
|
},
|
|
64
|
+
"bugs": {
|
|
65
|
+
"url": "https://github.com/withguuey/guuey-sdks/issues"
|
|
66
|
+
},
|
|
67
67
|
"scripts": {
|
|
68
68
|
"build": "tsc -p tsconfig.build.json",
|
|
69
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,18 +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
16
|
createWebAdapters,
|
|
15
17
|
localStorageThreadStore,
|
|
16
18
|
webGenerateId,
|
|
17
19
|
fetchStreamTransport,
|
|
18
|
-
AgentResponseError,
|
|
19
20
|
type CreateWebAdaptersOptions,
|
|
20
|
-
} 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";
|
|
21
37
|
export {
|
|
22
38
|
fetchThreadHistory,
|
|
23
39
|
threadHistoryRowsToMessages,
|
|
@@ -25,13 +41,13 @@ export {
|
|
|
25
41
|
HistoryUnauthorizedError,
|
|
26
42
|
type ThreadHistoryRow,
|
|
27
43
|
type ThreadHistoryFetchOptions,
|
|
28
|
-
} from "./history";
|
|
29
|
-
export { ingestMessageFrame } from "./blocks";
|
|
44
|
+
} from "./history.js";
|
|
45
|
+
export { ingestMessageFrame } from "./blocks.js";
|
|
30
46
|
// Pure block-walk / resource-narrowing helpers for a block-preserving renderer
|
|
31
47
|
// (shared by Studio's `AgentBlocks` and Portal-web's agent chat). React-free.
|
|
32
48
|
// Transcript labeling/ordering helpers (mount narrowing itself moved to
|
|
33
49
|
// @guuey/mcp-apps-host — the SEP-1865 Host role package; import it directly).
|
|
34
|
-
export { sortHistoryCards, toolNameFor } from "./history";
|
|
50
|
+
export { sortHistoryCards, toolNameFor } from "./history.js";
|
|
35
51
|
// Re-export the AgJSON types the block-preserving transcript surfaces, so
|
|
36
52
|
// consumers can name `reduceResult` / block types without a direct
|
|
37
53
|
// `@silverprotocol/core` import.
|
|
@@ -51,4 +67,4 @@ export type {
|
|
|
51
67
|
HistoryLoadResult,
|
|
52
68
|
UseAgentInvokeOptions,
|
|
53
69
|
UseAgentInvokeReturn,
|
|
54
|
-
} 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.
|