@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.
Files changed (47) hide show
  1. package/README.md +54 -7
  2. package/dist/error-codes.d.ts +87 -0
  3. package/dist/error-codes.d.ts.map +1 -0
  4. package/dist/error-codes.js +82 -0
  5. package/dist/errors.d.ts +39 -0
  6. package/dist/errors.d.ts.map +1 -0
  7. package/dist/errors.js +36 -0
  8. package/dist/history.d.ts +1 -1
  9. package/dist/history.d.ts.map +1 -1
  10. package/dist/index.d.ts +13 -7
  11. package/dist/index.d.ts.map +1 -1
  12. package/dist/index.js +31 -6
  13. package/dist/invoke-turn.d.ts +101 -0
  14. package/dist/invoke-turn.d.ts.map +1 -0
  15. package/dist/invoke-turn.js +124 -0
  16. package/dist/react.d.ts +1 -1
  17. package/dist/react.d.ts.map +1 -1
  18. package/dist/react.js +1 -1
  19. package/dist/saturation-retry.d.ts +101 -0
  20. package/dist/saturation-retry.d.ts.map +1 -0
  21. package/dist/saturation-retry.js +207 -0
  22. package/dist/sse.d.ts +1 -1
  23. package/dist/sse.d.ts.map +1 -1
  24. package/dist/transport.d.ts +90 -0
  25. package/dist/transport.d.ts.map +1 -0
  26. package/dist/transport.js +189 -0
  27. package/dist/types.d.ts +101 -3
  28. package/dist/types.d.ts.map +1 -1
  29. package/dist/useAgentInvoke.d.ts +29 -1
  30. package/dist/useAgentInvoke.d.ts.map +1 -1
  31. package/dist/useAgentInvoke.js +297 -86
  32. package/dist/web-adapters.d.ts +30 -34
  33. package/dist/web-adapters.d.ts.map +1 -1
  34. package/dist/web-adapters.js +72 -123
  35. package/package.json +11 -5
  36. package/src/error-codes.ts +89 -0
  37. package/src/errors.ts +35 -0
  38. package/src/history.ts +1 -1
  39. package/src/index.ts +50 -10
  40. package/src/invoke-turn.ts +187 -0
  41. package/src/react.ts +7 -1
  42. package/src/saturation-retry.ts +247 -0
  43. package/src/sse.ts +1 -1
  44. package/src/transport.ts +260 -0
  45. package/src/types.ts +102 -3
  46. package/src/useAgentInvoke.ts +288 -86
  47. package/src/web-adapters.ts +92 -134
@@ -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
- yield* fetchStreamTransport(req, token, getGuestSecret ? getGuestSecret() : null);
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.1",
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.3.1"
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";