@guuey/agent-client 0.3.1 → 0.5.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 +54 -7
- package/dist/error-codes.d.ts +87 -0
- package/dist/error-codes.d.ts.map +1 -0
- package/dist/error-codes.js +82 -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 +13 -7
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +31 -6
- package/dist/invoke-turn.d.ts +101 -0
- package/dist/invoke-turn.d.ts.map +1 -0
- package/dist/invoke-turn.js +124 -0
- 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 +101 -0
- package/dist/saturation-retry.d.ts.map +1 -0
- package/dist/saturation-retry.js +207 -0
- package/dist/sse.d.ts +1 -1
- package/dist/sse.d.ts.map +1 -1
- package/dist/transport.d.ts +90 -0
- package/dist/transport.d.ts.map +1 -0
- package/dist/transport.js +189 -0
- package/dist/types.d.ts +101 -3
- package/dist/types.d.ts.map +1 -1
- package/dist/useAgentInvoke.d.ts +29 -1
- package/dist/useAgentInvoke.d.ts.map +1 -1
- package/dist/useAgentInvoke.js +297 -86
- package/dist/web-adapters.d.ts +30 -34
- package/dist/web-adapters.d.ts.map +1 -1
- package/dist/web-adapters.js +72 -123
- package/package.json +11 -5
- package/src/error-codes.ts +89 -0
- package/src/errors.ts +35 -0
- package/src/history.ts +1 -1
- package/src/index.ts +50 -10
- package/src/invoke-turn.ts +187 -0
- package/src/react.ts +7 -1
- package/src/saturation-retry.ts +247 -0
- package/src/sse.ts +1 -1
- package/src/transport.ts +260 -0
- package/src/types.ts +102 -3
- package/src/useAgentInvoke.ts +288 -86
- package/src/web-adapters.ts +92 -134
package/dist/web-adapters.js
CHANGED
|
@@ -6,25 +6,9 @@
|
|
|
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
|
-
* 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 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 { fetchStreamTransport, sendableGuestSecret, GUEST_HEADER } from "./transport.js";
|
|
28
12
|
/** Persists the threadId in `window.localStorage` (synchronously). */
|
|
29
13
|
export const localStorageThreadStore = {
|
|
30
14
|
load(key) {
|
|
@@ -55,108 +39,6 @@ export function webGenerateId() {
|
|
|
55
39
|
}
|
|
56
40
|
return `cmid-${Date.now()}-${Math.floor(Math.random() * 1e9)}`;
|
|
57
41
|
}
|
|
58
|
-
/**
|
|
59
|
-
* Header carrying a caller-owned anonymous guest secret. A LOCAL MIRROR of the
|
|
60
|
-
* two server-side constants — the pod's `GUEST_HEADER_NAME`
|
|
61
|
-
* (`backend/services/nocode-runtime/src/identity.ts`) and the read plane's
|
|
62
|
-
* `GUEST_HEADER` (`backend/amplify/functions/publicApi/identity.ts`) — because
|
|
63
|
-
* this is a published npm package and cannot take a `@guuey-private` dep (same
|
|
64
|
-
* arrangement as `@guuey/host`'s mirrored fs-contract constants). The string is
|
|
65
|
-
* a wire contract: both planes already advertise it in
|
|
66
|
-
* `Access-Control-Allow-Headers`, so changing it is a breaking protocol change,
|
|
67
|
-
* not a rename.
|
|
68
|
-
*/
|
|
69
|
-
const GUEST_HEADER = "x-guuey-guest";
|
|
70
|
-
/**
|
|
71
|
-
* A well-formed guest secret: exactly 32 bytes as 64 LOWERCASE hex chars —
|
|
72
|
-
* the shape `crypto.getRandomValues` + hex-encoding mints.
|
|
73
|
-
*
|
|
74
|
-
* Deliberately stricter than the server's `/^[a-f0-9]{64}$/i` (pod
|
|
75
|
-
* `identity.ts`, publicApi `identity.ts`): both sides lowercase before
|
|
76
|
-
* hashing, so an uppercase secret would in fact be accepted, but the only
|
|
77
|
-
* supported mint path emits lowercase and a non-canonical value means the
|
|
78
|
-
* caller's storage is not what this adapter expects. Anything that fails is
|
|
79
|
-
* IGNORED — the request falls through to cookie mode rather than sending a
|
|
80
|
-
* secret the two identity planes might key differently.
|
|
81
|
-
*/
|
|
82
|
-
const GUEST_SECRET_RE = /^[0-9a-f]{64}$/;
|
|
83
|
-
/**
|
|
84
|
-
* Narrow a caller-supplied guest secret to a value that is safe to put on the
|
|
85
|
-
* wire, or `null`. The single gate for the header: every write of
|
|
86
|
-
* {@link GUEST_HEADER} in this module goes through it, so a malformed secret
|
|
87
|
-
* can never reach a request. The value is never logged (here or anywhere on
|
|
88
|
-
* this path) — it IS the anonymous identity, so a leak is an impersonation.
|
|
89
|
-
*/
|
|
90
|
-
function sendableGuestSecret(secret) {
|
|
91
|
-
return typeof secret === "string" && GUEST_SECRET_RE.test(secret) ? secret : null;
|
|
92
|
-
}
|
|
93
|
-
/**
|
|
94
|
-
* Web SSE transport. Exactly ONE identity carrier per request, in order:
|
|
95
|
-
*
|
|
96
|
-
* 1. `accessToken` → `Authorization: Bearer` — the pod identifies the caller
|
|
97
|
-
* by their verified access token (the same identity the history read
|
|
98
|
-
* plane uses, so persisted threads round-trip on reload).
|
|
99
|
-
* 2. a well-formed `guestSecret` → `x-guuey-guest` — the caller owns and
|
|
100
|
-
* persists its own anonymous secret. The path for hosts with no usable
|
|
101
|
-
* cookie jar: React-Native, and the embedded widget, whose third-party
|
|
102
|
-
* iframe cannot rely on the pod's cookie surviving browser partitioning.
|
|
103
|
-
* The pod never mints a cookie for a header client.
|
|
104
|
-
* 3. neither → `credentials: "include"`, which round-trips the HttpOnly
|
|
105
|
-
* `guuey_guest` cookie the pod mints for anonymous browser callers.
|
|
106
|
-
*
|
|
107
|
-
* Never two at once: a bearer wins over a guest secret, and a request that
|
|
108
|
-
* carries either header does NOT also send cookie credentials.
|
|
109
|
-
*
|
|
110
|
-
* Reads the body via `ReadableStream.getReader()` (browser).
|
|
111
|
-
*/
|
|
112
|
-
export async function* fetchStreamTransport(req, accessToken, guestSecret) {
|
|
113
|
-
const headers = {
|
|
114
|
-
"Content-Type": "application/json",
|
|
115
|
-
Accept: "text/event-stream",
|
|
116
|
-
};
|
|
117
|
-
const init = {
|
|
118
|
-
method: "POST",
|
|
119
|
-
signal: req.signal,
|
|
120
|
-
headers,
|
|
121
|
-
body: JSON.stringify(req.body),
|
|
122
|
-
};
|
|
123
|
-
const guest = sendableGuestSecret(guestSecret);
|
|
124
|
-
if (accessToken) {
|
|
125
|
-
headers.Authorization = `Bearer ${accessToken}`;
|
|
126
|
-
}
|
|
127
|
-
else if (guest) {
|
|
128
|
-
headers[GUEST_HEADER] = guest;
|
|
129
|
-
}
|
|
130
|
-
else {
|
|
131
|
-
init.credentials = "include";
|
|
132
|
-
}
|
|
133
|
-
const resp = await fetch(req.url, init);
|
|
134
|
-
if (!resp.ok || !resp.body) {
|
|
135
|
-
// Surface a structured pod error ({ code, message }) when present — e.g. a
|
|
136
|
-
// QUOTA_EXCEEDED 429 carries an upgrade message the UI should show. Fall
|
|
137
|
-
// back to the bare status for non-JSON failures.
|
|
138
|
-
const body = await resp.json().catch(() => null);
|
|
139
|
-
let message = `agent responded ${resp.status}`;
|
|
140
|
-
let code;
|
|
141
|
-
if (body !== null && typeof body === "object") {
|
|
142
|
-
if ("message" in body && typeof body.message === "string" && body.message) {
|
|
143
|
-
message = body.message;
|
|
144
|
-
}
|
|
145
|
-
if ("code" in body && typeof body.code === "string") {
|
|
146
|
-
code = body.code;
|
|
147
|
-
}
|
|
148
|
-
}
|
|
149
|
-
throw new AgentResponseError(message, resp.status, code);
|
|
150
|
-
}
|
|
151
|
-
const reader = resp.body.getReader();
|
|
152
|
-
const decoder = new TextDecoder();
|
|
153
|
-
for (;;) {
|
|
154
|
-
const { value, done } = await reader.read();
|
|
155
|
-
if (done)
|
|
156
|
-
break;
|
|
157
|
-
yield decoder.decode(value, { stream: true });
|
|
158
|
-
}
|
|
159
|
-
}
|
|
160
42
|
/**
|
|
161
43
|
* Build the web host-adapter bundle for {@link useAgentInvoke}. Pass an
|
|
162
44
|
* access-token resolver and/or a guest-secret resolver (plus the read-plane
|
|
@@ -167,10 +49,16 @@ export async function* fetchStreamTransport(req, accessToken, guestSecret) {
|
|
|
167
49
|
export function createWebAdapters(opts = {}) {
|
|
168
50
|
const { apiBaseUrl, getAccessToken, getGuestSecret } = opts;
|
|
169
51
|
const transport = async function* (req) {
|
|
170
|
-
const token = getAccessToken ? await getAccessToken() : null;
|
|
171
52
|
// Both candidates go to the transport; it owns the precedence (and the
|
|
172
53
|
// never-two-carriers rule) so there is exactly one place that decides.
|
|
173
|
-
|
|
54
|
+
// The bearer goes through as the PROVIDER, not a pre-resolved value:
|
|
55
|
+
// the transport re-asks it per attempt, so a cold-start retry after a
|
|
56
|
+
// backoff wait re-reads a fresh token instead of replaying one that may
|
|
57
|
+
// have expired during the wait (the same reason Portal's RN transport
|
|
58
|
+
// resolves inside its generator).
|
|
59
|
+
yield* fetchStreamTransport(req, null, getGuestSecret ? getGuestSecret() : null, {
|
|
60
|
+
getBearer: getAccessToken,
|
|
61
|
+
});
|
|
174
62
|
};
|
|
175
63
|
const adapters = {
|
|
176
64
|
storage: localStorageThreadStore,
|
|
@@ -310,3 +198,64 @@ export function createUiResourceReader(options) {
|
|
|
310
198
|
};
|
|
311
199
|
return createMcpUiResourceReader({ readResource });
|
|
312
200
|
}
|
|
201
|
+
/**
|
|
202
|
+
* Build the card action relay over guuey's authenticated `tools/call` proxy
|
|
203
|
+
* (guuey#158: `POST /v1/threads/:threadId/ui-action`) — the mirror of
|
|
204
|
+
* {@link createUiResourceReader}. Allowlisting, arm narrowing, and the
|
|
205
|
+
* never-reject contract live in `@guuey/mcp-apps-host`'s
|
|
206
|
+
* `createMcpUiActionRelay`; only the transport is guuey-shaped. The proxy
|
|
207
|
+
* owns EVERYTHING trust-shaped (identity, thread ownership, the
|
|
208
|
+
* locator-to-thread guard, its own server-side allowlist, the per-user
|
|
209
|
+
* federation mint) — and every non-OK here collapses to `undefined`, which
|
|
210
|
+
* the host relay answers in-band as an `isError` result, never a thrown
|
|
211
|
+
* error into the sandbox bridge.
|
|
212
|
+
*/
|
|
213
|
+
export function createUiActionRelay(options) {
|
|
214
|
+
const fetchImpl = options.fetchImpl ?? fetch;
|
|
215
|
+
const callTool = async (uri, name, args) => {
|
|
216
|
+
const headers = { "content-type": "application/json" };
|
|
217
|
+
const token = options.getAccessToken ? await options.getAccessToken() : null;
|
|
218
|
+
const guest = sendableGuestSecret(options.guestSecret);
|
|
219
|
+
if (token) {
|
|
220
|
+
headers["authorization"] = `Bearer ${token}`;
|
|
221
|
+
}
|
|
222
|
+
else if (guest) {
|
|
223
|
+
headers[GUEST_HEADER] = guest;
|
|
224
|
+
}
|
|
225
|
+
const requestUrl = `${options.apiBaseUrl}/threads/${encodeURIComponent(options.threadId)}/ui-action`;
|
|
226
|
+
const body = JSON.stringify({ uri, name, ...(args !== undefined ? { arguments: args } : {}) });
|
|
227
|
+
let res;
|
|
228
|
+
try {
|
|
229
|
+
res = await fetchImpl(requestUrl, { method: "POST", headers, body });
|
|
230
|
+
}
|
|
231
|
+
catch {
|
|
232
|
+
return undefined; // transport failure — the host relay answers in-band
|
|
233
|
+
}
|
|
234
|
+
// One forceRefresh retry on 401 with a bearer in play — the same
|
|
235
|
+
// expired-but-refreshable recovery the reader performs.
|
|
236
|
+
if (res.status === 401 && options.getAccessToken) {
|
|
237
|
+
const fresh = await options.getAccessToken({ forceRefresh: true }).catch(() => null);
|
|
238
|
+
if (fresh) {
|
|
239
|
+
try {
|
|
240
|
+
res = await fetchImpl(requestUrl, {
|
|
241
|
+
method: "POST",
|
|
242
|
+
headers: { ...headers, authorization: `Bearer ${fresh}` },
|
|
243
|
+
body,
|
|
244
|
+
});
|
|
245
|
+
}
|
|
246
|
+
catch {
|
|
247
|
+
return undefined;
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
if (!res.ok)
|
|
252
|
+
return undefined;
|
|
253
|
+
try {
|
|
254
|
+
return (await res.json());
|
|
255
|
+
}
|
|
256
|
+
catch {
|
|
257
|
+
return undefined;
|
|
258
|
+
}
|
|
259
|
+
};
|
|
260
|
+
return createMcpUiActionRelay({ callTool });
|
|
261
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@guuey/agent-client",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.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",
|
|
@@ -25,11 +25,17 @@
|
|
|
25
25
|
"types": "./dist/react.d.ts",
|
|
26
26
|
"import": "./dist/react.js",
|
|
27
27
|
"default": "./dist/react.js"
|
|
28
|
+
},
|
|
29
|
+
"./transport": {
|
|
30
|
+
"react-native": "./src/transport.ts",
|
|
31
|
+
"types": "./dist/transport.d.ts",
|
|
32
|
+
"import": "./dist/transport.js",
|
|
33
|
+
"default": "./dist/transport.js"
|
|
28
34
|
}
|
|
29
35
|
},
|
|
30
36
|
"dependencies": {
|
|
31
37
|
"@silverprotocol/core": "0.4.1",
|
|
32
|
-
"@guuey/mcp-apps-host": "0.
|
|
38
|
+
"@guuey/mcp-apps-host": "0.5.0"
|
|
33
39
|
},
|
|
34
40
|
"peerDependencies": {
|
|
35
41
|
"react": ">=18"
|
|
@@ -56,14 +62,14 @@
|
|
|
56
62
|
"client"
|
|
57
63
|
],
|
|
58
64
|
"homepage": "https://guuey.com",
|
|
59
|
-
"bugs": {
|
|
60
|
-
"url": "https://github.com/loqu-co/guuey/issues"
|
|
61
|
-
},
|
|
62
65
|
"repository": {
|
|
63
66
|
"type": "git",
|
|
64
67
|
"url": "git+https://github.com/withguuey/guuey-sdks.git",
|
|
65
68
|
"directory": "packages/agent-client"
|
|
66
69
|
},
|
|
70
|
+
"bugs": {
|
|
71
|
+
"url": "https://github.com/withguuey/guuey-sdks/issues"
|
|
72
|
+
},
|
|
67
73
|
"scripts": {
|
|
68
74
|
"build": "tsc -p tsconfig.build.json",
|
|
69
75
|
"dev": "tsc --watch",
|
|
@@ -0,0 +1,89 @@
|
|
|
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
|
+
/**
|
|
31
|
+
* The agent's own definition declares `auth: 'required'` and the caller is
|
|
32
|
+
* anonymous — sign in and retry with a bearer. The snapshot-declared twin of
|
|
33
|
+
* {@link AGENT_ERROR_CODES.GUEST_ACCESS_DISABLED} (the app-record runtime
|
|
34
|
+
* override); either gate can refuse.
|
|
35
|
+
*/
|
|
36
|
+
AUTH_REQUIRED: "AUTH_REQUIRED",
|
|
37
|
+
/** The caller (or the app) is out of plan allowance — the upgrade prompt. */
|
|
38
|
+
QUOTA_EXCEEDED: "QUOTA_EXCEEDED",
|
|
39
|
+
/** The app hit its builder-set managed spend cap. */
|
|
40
|
+
MANAGED_SPEND_CAP: "MANAGED_SPEND_CAP",
|
|
41
|
+
/**
|
|
42
|
+
* The pod is at its concurrent-turn cap (scaling S1-F3). A 503 carrying a
|
|
43
|
+
* `Retry-After` hint, and the ONE code {@link fetchStreamTransport} retries
|
|
44
|
+
* by itself — see its docblock for the single-attempt rule.
|
|
45
|
+
*/
|
|
46
|
+
POD_SATURATED: "POD_SATURATED",
|
|
47
|
+
/**
|
|
48
|
+
* The pod took SIGTERM and refuses NEW turns while in-flight ones finish.
|
|
49
|
+
* Also a 503 + `Retry-After`, but deliberately NOT auto-retried (the
|
|
50
|
+
* endpoint pull re-routes the next request; retrying into the same pod is
|
|
51
|
+
* the one thing guaranteed not to help).
|
|
52
|
+
*/
|
|
53
|
+
DRAINING: "DRAINING",
|
|
54
|
+
/** Refused for this caller — e.g. the link-prompt dismiss route's byo-only rule. */
|
|
55
|
+
FORBIDDEN: "FORBIDDEN",
|
|
56
|
+
/** The turn ran past the pod's wall-clock budget. */
|
|
57
|
+
TIMEOUT: "TIMEOUT",
|
|
58
|
+
/** A guuey-side dependency failed (not the agent's own code). */
|
|
59
|
+
PLATFORM_ERROR: "PLATFORM_ERROR",
|
|
60
|
+
/** Unclassified pod failure. */
|
|
61
|
+
INTERNAL: "INTERNAL",
|
|
62
|
+
} as const;
|
|
63
|
+
|
|
64
|
+
/** One of the pod's wire codes — see {@link AGENT_ERROR_CODES}. */
|
|
65
|
+
export type AgentErrorCode = (typeof AGENT_ERROR_CODES)[keyof typeof AGENT_ERROR_CODES];
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* CLIENT-originated failure codes — minted by THIS SDK, never by the pod.
|
|
69
|
+
*
|
|
70
|
+
* Deliberately a SEPARATE constant from {@link AGENT_ERROR_CODES}: that
|
|
71
|
+
* object is a transcribed mirror of the runtime's wire vocabulary, guarded by
|
|
72
|
+
* the runtime-side `agent-client-codes.sync.test.ts` — adding a code the pod
|
|
73
|
+
* never emits there would both break the sync guard and lie about the wire.
|
|
74
|
+
* These codes surface through the SAME `errorCode` channel (it is a plain
|
|
75
|
+
* `string` for exactly this kind of growth), so consumers branch the same
|
|
76
|
+
* way; the split exists so each vocabulary keeps one honest owner.
|
|
77
|
+
*/
|
|
78
|
+
export const CLIENT_ERROR_CODES = {
|
|
79
|
+
/**
|
|
80
|
+
* The SSE stream went byte-silent mid-turn and bounded history probes never
|
|
81
|
+
* found the finished reply (guuey#192's stall watchdog giving up). The turn
|
|
82
|
+
* is over (`status` returns to `ready`); a retry or a reload may still find
|
|
83
|
+
* the reply if the backend completes later.
|
|
84
|
+
*/
|
|
85
|
+
STREAM_STALLED: "STREAM_STALLED",
|
|
86
|
+
} as const;
|
|
87
|
+
|
|
88
|
+
/** One of this SDK's client-originated codes — see {@link CLIENT_ERROR_CODES}. */
|
|
89
|
+
export type ClientErrorCode = (typeof CLIENT_ERROR_CODES)[keyof typeof CLIENT_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,54 @@ 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
|
+
// One agent turn as a pure async generator — the wire walk `useAgentInvoke`
|
|
12
|
+
// wraps, for hosts that drive their own turn state machine (guuey#186 G5).
|
|
13
|
+
export { invokeTurn, toInvokeUrl, type InvokeTurnEvent } from "./invoke-turn.js";
|
|
11
14
|
export {
|
|
15
|
+
createUiActionRelay,
|
|
16
|
+
type CreateUiActionRelayOptions,
|
|
12
17
|
createUiResourceReader,
|
|
13
18
|
type CreateUiResourceReaderOptions,
|
|
14
19
|
createWebAdapters,
|
|
15
20
|
localStorageThreadStore,
|
|
16
21
|
webGenerateId,
|
|
17
|
-
fetchStreamTransport,
|
|
18
|
-
AgentResponseError,
|
|
19
22
|
type CreateWebAdaptersOptions,
|
|
20
|
-
} from "./web-adapters";
|
|
23
|
+
} from "./web-adapters.js";
|
|
24
|
+
// The invoke transport + guest-identity wire pieces, in their own
|
|
25
|
+
// mcp-apps-host-free module. Consumers that want ONLY this graph (no
|
|
26
|
+
// host-role card layer riding along) import `@guuey/agent-client/transport`
|
|
27
|
+
// instead of the barrel — see that module's docblock (guuey#186 G2).
|
|
28
|
+
export {
|
|
29
|
+
fetchStreamTransport,
|
|
30
|
+
sendableGuestSecret,
|
|
31
|
+
GUEST_HEADER,
|
|
32
|
+
withActivityObserver,
|
|
33
|
+
type FetchStreamTransportOptions,
|
|
34
|
+
} from "./transport.js";
|
|
35
|
+
// The invoke-refusal retry wrappers, transport-agnostic: a host that brings
|
|
36
|
+
// its own `fetch` (Portal's React-Native transport) wraps them to wear the
|
|
37
|
+
// same semantics as the web transport instead of hand-rolling second copies.
|
|
38
|
+
// `parseRetryAfterSeconds` ships with them because filling
|
|
39
|
+
// `AgentResponseError.retryAfterSeconds` the same way is what makes the
|
|
40
|
+
// wrappers honour the pod's hint.
|
|
41
|
+
export {
|
|
42
|
+
withSaturationRetry,
|
|
43
|
+
withColdStartRetry,
|
|
44
|
+
parseRetryAfterSeconds,
|
|
45
|
+
type SaturationRetryOptions,
|
|
46
|
+
type ColdStartRetryOptions,
|
|
47
|
+
} from "./saturation-retry.js";
|
|
48
|
+
export { AgentResponseError } from "./errors.js";
|
|
49
|
+
// The pod's wire-code vocabulary, mirrored — branch on these instead of
|
|
50
|
+
// re-typing the string literals (see the module docblock for the sync guard).
|
|
51
|
+
export {
|
|
52
|
+
AGENT_ERROR_CODES,
|
|
53
|
+
type AgentErrorCode,
|
|
54
|
+
CLIENT_ERROR_CODES,
|
|
55
|
+
type ClientErrorCode,
|
|
56
|
+
} from "./error-codes.js";
|
|
21
57
|
export {
|
|
22
58
|
fetchThreadHistory,
|
|
23
59
|
threadHistoryRowsToMessages,
|
|
@@ -25,16 +61,19 @@ export {
|
|
|
25
61
|
HistoryUnauthorizedError,
|
|
26
62
|
type ThreadHistoryRow,
|
|
27
63
|
type ThreadHistoryFetchOptions,
|
|
28
|
-
} from "./history";
|
|
29
|
-
export { ingestMessageFrame } from "./blocks";
|
|
64
|
+
} from "./history.js";
|
|
65
|
+
export { ingestMessageFrame } from "./blocks.js";
|
|
30
66
|
// Pure block-walk / resource-narrowing helpers for a block-preserving renderer
|
|
31
67
|
// (shared by Studio's `AgentBlocks` and Portal-web's agent chat). React-free.
|
|
32
68
|
// Transcript labeling/ordering helpers (mount narrowing itself moved to
|
|
33
69
|
// @guuey/mcp-apps-host — the SEP-1865 Host role package; import it directly).
|
|
34
|
-
export { sortHistoryCards, toolNameFor } from "./history";
|
|
70
|
+
export { sortHistoryCards, toolNameFor } from "./history.js";
|
|
35
71
|
// Re-export the AgJSON types the block-preserving transcript surfaces, so
|
|
36
72
|
// consumers can name `reduceResult` / block types without a direct
|
|
37
|
-
// `@silverprotocol/core` import
|
|
73
|
+
// `@silverprotocol/core` import — and the `Reducer` CLASS beside them, so a
|
|
74
|
+
// host folding `invokeTurn`'s agEvents outside the hook builds its transcript
|
|
75
|
+
// on the same terms (the types alone forced the direct dep back, guuey#186 G4).
|
|
76
|
+
export { Reducer } from "@silverprotocol/core";
|
|
38
77
|
export type { AgEvent, AgReduceResult, AgMessage, AgBlock } from "@silverprotocol/core";
|
|
39
78
|
export type {
|
|
40
79
|
AgentMessage,
|
|
@@ -49,6 +88,7 @@ export type {
|
|
|
49
88
|
AgentInvokeHistoryAdapter,
|
|
50
89
|
AgentInvokeStatus,
|
|
51
90
|
HistoryLoadResult,
|
|
91
|
+
StallRecoveryOptions,
|
|
52
92
|
UseAgentInvokeOptions,
|
|
53
93
|
UseAgentInvokeReturn,
|
|
54
|
-
} from "./types";
|
|
94
|
+
} from "./types.js";
|