@guuey/agent-client 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +64 -0
- package/dist/block-ui.d.ts +112 -0
- package/dist/block-ui.d.ts.map +1 -0
- package/dist/block-ui.js +165 -0
- package/dist/blocks.d.ts +27 -0
- package/dist/blocks.d.ts.map +1 -0
- package/dist/blocks.js +32 -0
- package/dist/history.d.ts +62 -0
- package/dist/history.d.ts.map +1 -0
- package/dist/history.js +66 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +7 -0
- package/dist/react.d.ts +11 -0
- package/dist/react.d.ts.map +1 -0
- package/dist/react.js +9 -0
- package/dist/sse.d.ts +43 -0
- package/dist/sse.d.ts.map +1 -0
- package/dist/sse.js +107 -0
- package/dist/types.d.ts +145 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +12 -0
- package/dist/useAgentInvoke.d.ts +20 -0
- package/dist/useAgentInvoke.d.ts.map +1 -0
- package/dist/useAgentInvoke.js +282 -0
- package/dist/web-adapters.d.ts +59 -0
- package/dist/web-adapters.d.ts.map +1 -0
- package/dist/web-adapters.js +135 -0
- package/package.json +73 -0
- package/src/block-ui.ts +196 -0
- package/src/blocks.ts +32 -0
- package/src/history.ts +126 -0
- package/src/index.ts +55 -0
- package/src/react.ts +13 -0
- package/src/sse.ts +115 -0
- package/src/types.ts +153 -0
- package/src/useAgentInvoke.ts +303 -0
- package/src/web-adapters.ts +174 -0
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Web (browser / Next.js) host adapters for {@link useAgentInvoke}.
|
|
3
|
+
*
|
|
4
|
+
* Studio builds its bundle via {@link createWebAdapters}. The implementations
|
|
5
|
+
* touch `window.localStorage`, `crypto`, and `fetch` only inside their
|
|
6
|
+
* functions — never at module load — so this file is import-safe under SSR
|
|
7
|
+
* (the functions guard on `typeof window`).
|
|
8
|
+
*/
|
|
9
|
+
import type {
|
|
10
|
+
AgentInvokeAdapters,
|
|
11
|
+
InvokeRequest,
|
|
12
|
+
InvokeTransport,
|
|
13
|
+
ThreadIdStore,
|
|
14
|
+
} from "./types";
|
|
15
|
+
import { fetchThreadHistory } from "./history";
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Thrown when the pod returns a non-2xx status on `/agent/invoke` (before any
|
|
19
|
+
* SSE stream opens). Carries the pod's structured `{ code, message }` when
|
|
20
|
+
* present — e.g. a `QUOTA_EXCEEDED` 429 whose message ("…reached its plan
|
|
21
|
+
* generation limit…") the chat UI should surface — falling back to the bare
|
|
22
|
+
* status for non-JSON failures.
|
|
23
|
+
*/
|
|
24
|
+
export class AgentResponseError extends Error {
|
|
25
|
+
constructor(
|
|
26
|
+
message: string,
|
|
27
|
+
readonly status: number,
|
|
28
|
+
readonly code?: string,
|
|
29
|
+
) {
|
|
30
|
+
super(message);
|
|
31
|
+
this.name = "AgentResponseError";
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** Persists the threadId in `window.localStorage` (synchronously). */
|
|
36
|
+
export const localStorageThreadStore: ThreadIdStore = {
|
|
37
|
+
load(key) {
|
|
38
|
+
if (typeof window === "undefined") return null;
|
|
39
|
+
try {
|
|
40
|
+
return window.localStorage.getItem(key);
|
|
41
|
+
} catch {
|
|
42
|
+
return null;
|
|
43
|
+
}
|
|
44
|
+
},
|
|
45
|
+
save(key, threadId) {
|
|
46
|
+
if (typeof window === "undefined") return;
|
|
47
|
+
try {
|
|
48
|
+
window.localStorage.setItem(key, threadId);
|
|
49
|
+
} catch {
|
|
50
|
+
/* private mode / blocked storage — threadId stays in-memory only */
|
|
51
|
+
}
|
|
52
|
+
},
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
/** Crypto-strong client-message id, with a non-crypto fallback. */
|
|
56
|
+
export function webGenerateId(): string {
|
|
57
|
+
if (typeof crypto !== "undefined" && "randomUUID" in crypto) {
|
|
58
|
+
return crypto.randomUUID();
|
|
59
|
+
}
|
|
60
|
+
return `cmid-${Date.now()}-${Math.floor(Math.random() * 1e9)}`;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Web SSE transport. When `accessToken` is present the pod identifies the
|
|
65
|
+
* caller by their verified Cognito access token (the same identity the
|
|
66
|
+
* history read plane uses, so persisted threads round-trip on reload).
|
|
67
|
+
* Otherwise it falls back to `credentials: "include"`, which round-trips the
|
|
68
|
+
* HttpOnly `guuey_guest` cookie the pod mints for anonymous browser callers.
|
|
69
|
+
* Reads the body via `ReadableStream.getReader()` (browser).
|
|
70
|
+
*/
|
|
71
|
+
export async function* fetchStreamTransport(
|
|
72
|
+
req: InvokeRequest,
|
|
73
|
+
accessToken?: string | null,
|
|
74
|
+
): AsyncGenerator<string> {
|
|
75
|
+
const headers: Record<string, string> = {
|
|
76
|
+
"Content-Type": "application/json",
|
|
77
|
+
Accept: "text/event-stream",
|
|
78
|
+
};
|
|
79
|
+
const init: RequestInit = {
|
|
80
|
+
method: "POST",
|
|
81
|
+
signal: req.signal,
|
|
82
|
+
headers,
|
|
83
|
+
body: JSON.stringify(req.body),
|
|
84
|
+
};
|
|
85
|
+
if (accessToken) {
|
|
86
|
+
headers.Authorization = `Bearer ${accessToken}`;
|
|
87
|
+
} else {
|
|
88
|
+
init.credentials = "include";
|
|
89
|
+
}
|
|
90
|
+
const resp = await fetch(req.url, init);
|
|
91
|
+
if (!resp.ok || !resp.body) {
|
|
92
|
+
// Surface a structured pod error ({ code, message }) when present — e.g. a
|
|
93
|
+
// QUOTA_EXCEEDED 429 carries an upgrade message the UI should show. Fall
|
|
94
|
+
// back to the bare status for non-JSON failures.
|
|
95
|
+
const body: unknown = await resp.json().catch(() => null);
|
|
96
|
+
let message = `agent responded ${resp.status}`;
|
|
97
|
+
let code: string | undefined;
|
|
98
|
+
if (body !== null && typeof body === "object") {
|
|
99
|
+
if ("message" in body && typeof body.message === "string" && body.message) {
|
|
100
|
+
message = body.message;
|
|
101
|
+
}
|
|
102
|
+
if ("code" in body && typeof body.code === "string") {
|
|
103
|
+
code = body.code;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
throw new AgentResponseError(message, resp.status, code);
|
|
107
|
+
}
|
|
108
|
+
const reader = resp.body.getReader();
|
|
109
|
+
const decoder = new TextDecoder();
|
|
110
|
+
for (;;) {
|
|
111
|
+
const { value, done } = await reader.read();
|
|
112
|
+
if (done) break;
|
|
113
|
+
yield decoder.decode(value, { stream: true });
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
export interface CreateWebAdaptersOptions {
|
|
118
|
+
/**
|
|
119
|
+
* Public read-plane base (ending in `/v1`) for transcript history. When
|
|
120
|
+
* omitted, no history adapter is installed and reloads start empty.
|
|
121
|
+
*/
|
|
122
|
+
apiBaseUrl?: string;
|
|
123
|
+
/**
|
|
124
|
+
* Resolve the caller's Cognito access token (fresh), or `null` when signed
|
|
125
|
+
* out. When a token is present the chat transport AND the history read
|
|
126
|
+
* authenticate as that user, so a reload restores the transcript. Without
|
|
127
|
+
* a token the transport falls back to the guest cookie and history is
|
|
128
|
+
* skipped — the read plane can't identify a cookie-only browser caller
|
|
129
|
+
* (it reads the `x-guuey-guest` header or a Bearer, not the HttpOnly
|
|
130
|
+
* guest cookie), so there is no identity to replay.
|
|
131
|
+
*/
|
|
132
|
+
getAccessToken?: () => Promise<string | null>;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Build the web host-adapter bundle for {@link useAgentInvoke}. Pass an
|
|
137
|
+
* access-token resolver (and the read-plane base) to authenticate the chat
|
|
138
|
+
* transport and enable transcript restore on reload; omit them for an
|
|
139
|
+
* anonymous, history-less bundle.
|
|
140
|
+
*/
|
|
141
|
+
export function createWebAdapters(
|
|
142
|
+
opts: CreateWebAdaptersOptions = {},
|
|
143
|
+
): AgentInvokeAdapters {
|
|
144
|
+
const { apiBaseUrl, getAccessToken } = opts;
|
|
145
|
+
|
|
146
|
+
const transport: InvokeTransport = async function* (req) {
|
|
147
|
+
const token = getAccessToken ? await getAccessToken() : null;
|
|
148
|
+
yield* fetchStreamTransport(req, token);
|
|
149
|
+
};
|
|
150
|
+
|
|
151
|
+
const adapters: AgentInvokeAdapters = {
|
|
152
|
+
storage: localStorageThreadStore,
|
|
153
|
+
generateId: webGenerateId,
|
|
154
|
+
transport,
|
|
155
|
+
};
|
|
156
|
+
|
|
157
|
+
if (apiBaseUrl && getAccessToken) {
|
|
158
|
+
adapters.history = {
|
|
159
|
+
load: async (threadId) => {
|
|
160
|
+
const token = await getAccessToken();
|
|
161
|
+
// No readable identity → leave the chat empty (skip) rather than
|
|
162
|
+
// `gone`, which would clear the persisted threadId.
|
|
163
|
+
if (!token) return { messages: [] };
|
|
164
|
+
return fetchThreadHistory({
|
|
165
|
+
baseUrl: apiBaseUrl,
|
|
166
|
+
threadId,
|
|
167
|
+
requestInit: { headers: { Authorization: `Bearer ${token}` } },
|
|
168
|
+
});
|
|
169
|
+
},
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
return adapters;
|
|
174
|
+
}
|