@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/src/transport.ts
ADDED
|
@@ -0,0 +1,260 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The web SSE invoke transport, and the guest-identity wire pieces it shares
|
|
3
|
+
* with the read-plane adapters.
|
|
4
|
+
*
|
|
5
|
+
* Its own module (rather than living in `./web-adapters.ts`) for the same
|
|
6
|
+
* reason `AgentResponseError` lives in `./errors.ts`, one level down: the
|
|
7
|
+
* web-adapter bundle takes VALUE imports of `@guuey/mcp-apps-host` (the
|
|
8
|
+
* ui-resource reader / action-relay assemblies), so a consumer that only
|
|
9
|
+
* wants the transport — a custom chat surface with its own card layer, or
|
|
10
|
+
* none — would drag the whole host-role graph into its build, and any
|
|
11
|
+
* version skew between the two packages becomes that consumer's build
|
|
12
|
+
* failure (guuey#186 G2). This module's import closure is
|
|
13
|
+
* `types`/`errors`/`saturation-retry` only; `./web-adapters.ts` imports
|
|
14
|
+
* from HERE, never the reverse, and `@guuey/agent-client/transport`
|
|
15
|
+
* publishes exactly this graph.
|
|
16
|
+
*/
|
|
17
|
+
import type { InvokeRequest, InvokeTransport } from "./types.js";
|
|
18
|
+
import { AgentResponseError } from "./errors.js";
|
|
19
|
+
import {
|
|
20
|
+
parseRetryAfterSeconds,
|
|
21
|
+
withColdStartRetry,
|
|
22
|
+
withSaturationRetry,
|
|
23
|
+
type ColdStartRetryOptions,
|
|
24
|
+
type SaturationRetryOptions,
|
|
25
|
+
} from "./saturation-retry.js";
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Header carrying a caller-owned anonymous guest secret. A LOCAL MIRROR of the
|
|
29
|
+
* two server-side constants — the pod's `GUEST_HEADER_NAME`
|
|
30
|
+
* (`backend/services/nocode-runtime/src/identity.ts`) and the read plane's
|
|
31
|
+
* `GUEST_HEADER` (`backend/amplify/functions/publicApi/identity.ts`) — because
|
|
32
|
+
* this is a published npm package and cannot take a `@guuey-private` dep (same
|
|
33
|
+
* arrangement as `@guuey/host`'s mirrored fs-contract constants). The string is
|
|
34
|
+
* a wire contract: both planes already advertise it in
|
|
35
|
+
* `Access-Control-Allow-Headers`, so changing it is a breaking protocol change,
|
|
36
|
+
* not a rename.
|
|
37
|
+
*/
|
|
38
|
+
export const GUEST_HEADER = "x-guuey-guest";
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* A well-formed guest secret: exactly 32 bytes as 64 LOWERCASE hex chars —
|
|
42
|
+
* the shape `crypto.getRandomValues` + hex-encoding mints.
|
|
43
|
+
*
|
|
44
|
+
* Deliberately stricter than the server's `/^[a-f0-9]{64}$/i` (pod
|
|
45
|
+
* `identity.ts`, publicApi `identity.ts`): both sides lowercase before
|
|
46
|
+
* hashing, so an uppercase secret would in fact be accepted, but the only
|
|
47
|
+
* supported mint path emits lowercase and a non-canonical value means the
|
|
48
|
+
* caller's storage is not what this adapter expects. Anything that fails is
|
|
49
|
+
* IGNORED — the request falls through to cookie mode rather than sending a
|
|
50
|
+
* secret the two identity planes might key differently.
|
|
51
|
+
*/
|
|
52
|
+
const GUEST_SECRET_RE = /^[0-9a-f]{64}$/;
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Narrow a caller-supplied guest secret to a value that is safe to put on the
|
|
56
|
+
* wire, or `null`. The single gate for the header: every write of
|
|
57
|
+
* {@link GUEST_HEADER} in this package goes through it, so a malformed secret
|
|
58
|
+
* can never reach a request. The value is never logged (here or anywhere on
|
|
59
|
+
* this path) — it IS the anonymous identity, so a leak is an impersonation.
|
|
60
|
+
*/
|
|
61
|
+
export function sendableGuestSecret(secret: string | null | undefined): string | null {
|
|
62
|
+
return typeof secret === "string" && GUEST_SECRET_RE.test(secret) ? secret : null;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Is this request a cross-origin call from a browser document? Only then can
|
|
67
|
+
* a fetch `TypeError` be a CORS refusal worth hinting about. `location` is
|
|
68
|
+
* read via `typeof` so Node and React Native (where CORS does not exist)
|
|
69
|
+
* answer false; an unparseable URL answers false rather than throwing from
|
|
70
|
+
* inside error handling.
|
|
71
|
+
*/
|
|
72
|
+
function isCrossOriginBrowserCall(url: string): boolean {
|
|
73
|
+
if (typeof location === "undefined") return false;
|
|
74
|
+
try {
|
|
75
|
+
return new URL(url).origin !== location.origin;
|
|
76
|
+
} catch {
|
|
77
|
+
return false;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* One invoke attempt: opens the request and yields decoded SSE chunks.
|
|
83
|
+
* {@link fetchStreamTransport} wraps this with the shared saturation retry —
|
|
84
|
+
* every behaviour below is per-attempt.
|
|
85
|
+
*
|
|
86
|
+
* Exactly ONE identity carrier per request, in order:
|
|
87
|
+
*
|
|
88
|
+
* 1. `accessToken` → `Authorization: Bearer` — the pod identifies the caller
|
|
89
|
+
* by their verified access token (the same identity the history read
|
|
90
|
+
* plane uses, so persisted threads round-trip on reload).
|
|
91
|
+
* 2. a well-formed `guestSecret` → `x-guuey-guest` — the caller owns and
|
|
92
|
+
* persists its own anonymous secret. The path for hosts with no usable
|
|
93
|
+
* cookie jar: React-Native, and the embedded widget, whose third-party
|
|
94
|
+
* iframe cannot rely on the pod's cookie surviving browser partitioning.
|
|
95
|
+
* The pod never mints a cookie for a header client.
|
|
96
|
+
* 3. neither → `credentials: "include"`, which round-trips the HttpOnly
|
|
97
|
+
* `guuey_guest` cookie the pod mints for anonymous browser callers.
|
|
98
|
+
*
|
|
99
|
+
* Never two at once: a bearer wins over a guest secret, and a request that
|
|
100
|
+
* carries either header does NOT also send cookie credentials.
|
|
101
|
+
*
|
|
102
|
+
* Reads the body via `ReadableStream.getReader()` (browser).
|
|
103
|
+
*/
|
|
104
|
+
async function* streamInvokeOnce(
|
|
105
|
+
req: InvokeRequest,
|
|
106
|
+
accessToken?: string | null,
|
|
107
|
+
guestSecret?: string | null,
|
|
108
|
+
): AsyncGenerator<string> {
|
|
109
|
+
const headers: Record<string, string> = {
|
|
110
|
+
"Content-Type": "application/json",
|
|
111
|
+
Accept: "text/event-stream",
|
|
112
|
+
};
|
|
113
|
+
const init: RequestInit = {
|
|
114
|
+
method: "POST",
|
|
115
|
+
signal: req.signal,
|
|
116
|
+
headers,
|
|
117
|
+
body: JSON.stringify(req.body),
|
|
118
|
+
};
|
|
119
|
+
const guest = sendableGuestSecret(guestSecret);
|
|
120
|
+
if (accessToken) {
|
|
121
|
+
headers.Authorization = `Bearer ${accessToken}`;
|
|
122
|
+
} else if (guest) {
|
|
123
|
+
headers[GUEST_HEADER] = guest;
|
|
124
|
+
} else {
|
|
125
|
+
init.credentials = "include";
|
|
126
|
+
}
|
|
127
|
+
let resp: Response;
|
|
128
|
+
try {
|
|
129
|
+
resp = await fetch(req.url, init);
|
|
130
|
+
} catch (err) {
|
|
131
|
+
// A network-level TypeError on a CROSS-ORIGIN invoke from a browser is,
|
|
132
|
+
// in practice, very often a missing allowedDomains entry — the CORS
|
|
133
|
+
// preflight failed and the web platform deliberately reports nothing
|
|
134
|
+
// more specific (guuey#186 Gap 2: the console.ggui.ai embed lost real
|
|
135
|
+
// time to an unexplained "Failed to fetch"). The error stays a
|
|
136
|
+
// TypeError with the original as `cause`; the added sentence is a HINT,
|
|
137
|
+
// not a diagnosis — offline, DNS and CSP failures throw the same shape.
|
|
138
|
+
// Same-origin calls and non-browser runtimes (no `location`) cannot be
|
|
139
|
+
// CORS refusals, so they pass through untouched.
|
|
140
|
+
if (err instanceof TypeError && isCrossOriginBrowserCall(req.url)) {
|
|
141
|
+
throw new TypeError(
|
|
142
|
+
`${err.message} — if this is a browser embed, check the app's allowedDomains (the CORS allowlist must include this page's origin)`,
|
|
143
|
+
{ cause: err },
|
|
144
|
+
);
|
|
145
|
+
}
|
|
146
|
+
throw err;
|
|
147
|
+
}
|
|
148
|
+
if (!resp.ok || !resp.body) {
|
|
149
|
+
// Surface a structured pod error ({ code, message }) when present — e.g. a
|
|
150
|
+
// QUOTA_EXCEEDED 429 carries an upgrade message the UI should show. Fall
|
|
151
|
+
// back to the bare status for non-JSON failures.
|
|
152
|
+
const body: unknown = await resp.json().catch(() => null);
|
|
153
|
+
let message = `agent responded ${resp.status}`;
|
|
154
|
+
let code: string | undefined;
|
|
155
|
+
if (body !== null && typeof body === "object") {
|
|
156
|
+
if ("message" in body && typeof body.message === "string" && body.message) {
|
|
157
|
+
message = body.message;
|
|
158
|
+
}
|
|
159
|
+
if ("code" in body && typeof body.code === "string") {
|
|
160
|
+
code = body.code;
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
throw new AgentResponseError(
|
|
164
|
+
message,
|
|
165
|
+
resp.status,
|
|
166
|
+
code,
|
|
167
|
+
parseRetryAfterSeconds(resp.headers.get("Retry-After")),
|
|
168
|
+
);
|
|
169
|
+
}
|
|
170
|
+
const reader = resp.body.getReader();
|
|
171
|
+
const decoder = new TextDecoder();
|
|
172
|
+
for (;;) {
|
|
173
|
+
const { value, done } = await reader.read();
|
|
174
|
+
if (done) break;
|
|
175
|
+
yield decoder.decode(value, { stream: true });
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/** Options for {@link fetchStreamTransport}. */
|
|
180
|
+
export interface FetchStreamTransportOptions extends SaturationRetryOptions {
|
|
181
|
+
/**
|
|
182
|
+
* Bounded retry on cold-start 503s — the envelope-less refusal an embed
|
|
183
|
+
* eats for ~30–60s after the agent redeploys (guuey#186 Gap 3). ON by
|
|
184
|
+
* default (small budget: 3 attempts, 2s/4s/8s) for parity with guuey's
|
|
185
|
+
* first-party embeds; pass `false` to disable, or options to re-budget.
|
|
186
|
+
* See {@link withColdStartRetry} for exactly what matches (and what
|
|
187
|
+
* deliberately stays with the saturation policy instead).
|
|
188
|
+
*/
|
|
189
|
+
coldStartRetry?: ColdStartRetryOptions | false;
|
|
190
|
+
/**
|
|
191
|
+
* Injectable bearer provider (guuey#186 Gap 4) — identity is a transport
|
|
192
|
+
* concern (see {@link InvokeTransport}: "owns headers + identity
|
|
193
|
+
* entirely"), and a harness or non-React host holds credentials in its own
|
|
194
|
+
* lifecycle, not in a closure minted once at page load. Resolved PER
|
|
195
|
+
* ATTEMPT, before each request — a retry after a backoff wait re-reads it,
|
|
196
|
+
* so a token that expired during the wait is refreshed rather than
|
|
197
|
+
* replayed. When present it takes precedence over the positional
|
|
198
|
+
* `accessToken`; resolving `null` falls through to the guest secret /
|
|
199
|
+
* cookie chain exactly as a null `accessToken` does (and carries the same
|
|
200
|
+
* silent-anonymous-downgrade hazard the `createWebAdapters` docs warn
|
|
201
|
+
* about). A throw propagates and fails the invoke — deliberately not
|
|
202
|
+
* caught, for the same reason as `getGuestSecret` there.
|
|
203
|
+
*/
|
|
204
|
+
getBearer?: () => string | null | Promise<string | null>;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* The web SSE transport: {@link streamInvokeOnce} under the shared
|
|
209
|
+
* {@link withSaturationRetry} wrapper, itself under {@link withColdStartRetry}.
|
|
210
|
+
* Every consumer of this transport (Studio, the widget, anything built on
|
|
211
|
+
* `createWebAdapters`) therefore inherits the single `POD_SATURATED` retry AND
|
|
212
|
+
* the bounded cold-start 503 retry, the same pair Portal's React-Native
|
|
213
|
+
* transport wears — see the wrappers' docblocks for which refusals retry,
|
|
214
|
+
* which deliberately do not, and why both retries are invisible to the hook.
|
|
215
|
+
* Both wrappers guard on "nothing yielded yet": once a chunk has streamed,
|
|
216
|
+
* NOTHING re-POSTs.
|
|
217
|
+
*/
|
|
218
|
+
export function fetchStreamTransport(
|
|
219
|
+
req: InvokeRequest,
|
|
220
|
+
accessToken?: string | null,
|
|
221
|
+
guestSecret?: string | null,
|
|
222
|
+
options: FetchStreamTransportOptions = {},
|
|
223
|
+
): AsyncIterable<string> {
|
|
224
|
+
const { getBearer } = options;
|
|
225
|
+
const once = async function* (attempt: InvokeRequest): AsyncGenerator<string> {
|
|
226
|
+
// Per-attempt resolution: each retry re-asks the provider (fresh token
|
|
227
|
+
// after a backoff wait) instead of replaying a captured one.
|
|
228
|
+
const bearer = getBearer ? await getBearer() : accessToken;
|
|
229
|
+
yield* streamInvokeOnce(attempt, bearer, guestSecret);
|
|
230
|
+
};
|
|
231
|
+
const saturated = withSaturationRetry(once, { sleep: options.sleep });
|
|
232
|
+
if (options.coldStartRetry === false) return saturated(req);
|
|
233
|
+
return withColdStartRetry(saturated, {
|
|
234
|
+
sleep: options.sleep,
|
|
235
|
+
...options.coldStartRetry,
|
|
236
|
+
})(req);
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/**
|
|
240
|
+
* Wrap a transport so every yielded chunk ALSO pings `onChunk` — the
|
|
241
|
+
* byte-level liveness signal `useAgentInvoke`'s stall watchdog runs on
|
|
242
|
+
* (guuey#192). Purely observational: chunks pass through unchanged, errors
|
|
243
|
+
* and completion propagate untouched, and the wrapper adds no timers of its
|
|
244
|
+
* own — the OBSERVER owns the clock, this module only reports activity. The
|
|
245
|
+
* first ping doubles as the "first byte seen" arming signal, which is why
|
|
246
|
+
* the watchdog never fires during a silent cold start: no bytes, no ping,
|
|
247
|
+
* no armed timer (that phase belongs to {@link withColdStartRetry} and the
|
|
248
|
+
* user's own abort).
|
|
249
|
+
*/
|
|
250
|
+
export function withActivityObserver(
|
|
251
|
+
transport: InvokeTransport,
|
|
252
|
+
onChunk: () => void,
|
|
253
|
+
): InvokeTransport {
|
|
254
|
+
return async function* observed(req: InvokeRequest): AsyncGenerator<string> {
|
|
255
|
+
for await (const chunk of transport(req)) {
|
|
256
|
+
onChunk();
|
|
257
|
+
yield chunk;
|
|
258
|
+
}
|
|
259
|
+
};
|
|
260
|
+
}
|
package/src/types.ts
CHANGED
|
@@ -19,6 +19,13 @@ import type { AgReduceResult, JsonValue } from "@silverprotocol/core";
|
|
|
19
19
|
export interface AgentMessage {
|
|
20
20
|
role: "user" | "assistant";
|
|
21
21
|
text: string;
|
|
22
|
+
/**
|
|
23
|
+
* The invoke body's `clientMessageId`, present on user turns this client
|
|
24
|
+
* sent live (the optimistic push tags it). History-rehydrated entries and
|
|
25
|
+
* assistant turns omit it. The transcript renderer's R0 send-lifecycle
|
|
26
|
+
* join key (`UseAgentInvokeReturn.sendStates`).
|
|
27
|
+
*/
|
|
28
|
+
clientMessageId?: string;
|
|
22
29
|
}
|
|
23
30
|
|
|
24
31
|
/**
|
|
@@ -127,6 +134,23 @@ export interface AgentInvokeAdapters {
|
|
|
127
134
|
history?: AgentInvokeHistoryAdapter;
|
|
128
135
|
}
|
|
129
136
|
|
|
137
|
+
/** Tuning for the guuey#192 stall watchdog — see {@link UseAgentInvokeOptions.stallRecovery}. */
|
|
138
|
+
export interface StallRecoveryOptions {
|
|
139
|
+
/**
|
|
140
|
+
* Byte-inactivity window (ms) before a history probe fires. Armed only
|
|
141
|
+
* AFTER the first byte of the turn — pre-first-byte silence is a legitimate
|
|
142
|
+
* cold start and never triggers. Every received chunk resets it.
|
|
143
|
+
* Default 25000.
|
|
144
|
+
*/
|
|
145
|
+
windowMs?: number;
|
|
146
|
+
/**
|
|
147
|
+
* Fruitless probes (history shows the turn still in flight, or no probe is
|
|
148
|
+
* possible) before the turn fails with `CLIENT_ERROR_CODES.STREAM_STALLED`.
|
|
149
|
+
* Any received byte resets the count. Default 4.
|
|
150
|
+
*/
|
|
151
|
+
probeAttempts?: number;
|
|
152
|
+
}
|
|
153
|
+
|
|
130
154
|
export interface UseAgentInvokeOptions {
|
|
131
155
|
/** Pod base URL (with or without a trailing `/agent/invoke`). Chat is disabled when null. */
|
|
132
156
|
endpointUrl: string | null;
|
|
@@ -141,6 +165,29 @@ export interface UseAgentInvokeOptions {
|
|
|
141
165
|
* reducer is never constructed and the text behaviour is byte-identical.
|
|
142
166
|
*/
|
|
143
167
|
preserveBlocks?: boolean;
|
|
168
|
+
/**
|
|
169
|
+
* Stall recovery for a half-dead stream (guuey#192). A connection that dies
|
|
170
|
+
* WITHOUT erroring (TCP alive, zero bytes, no `done`) would otherwise leave
|
|
171
|
+
* the turn suspended forever — a frozen cursor while the backend may have
|
|
172
|
+
* completed and persisted the reply. On by default; `false` restores the
|
|
173
|
+
* old hang-forever behaviour; an object tunes the window/attempts. Armed
|
|
174
|
+
* only after the first byte; probes are non-destructive (they read history
|
|
175
|
+
* WITHOUT touching the live stream) and adopt the finished reply from the
|
|
176
|
+
* history adapter when it is already persisted — see the watchdog notes in
|
|
177
|
+
* `useAgentInvoke`.
|
|
178
|
+
*/
|
|
179
|
+
stallRecovery?: false | StallRecoveryOptions;
|
|
180
|
+
/**
|
|
181
|
+
* Optional external abort authority (guuey#186 Gap 4): a host lifecycle —
|
|
182
|
+
* route change, dialog close, screen unmount — that must be able to stop
|
|
183
|
+
* an in-flight turn without holding the hook's own `abort()`. Composed
|
|
184
|
+
* with (never replacing) the hook's internal per-turn controller: when
|
|
185
|
+
* this signal aborts, the current turn aborts exactly as `abort()` would;
|
|
186
|
+
* the hook's `abort()`/`reset()` keep working regardless. Already-aborted
|
|
187
|
+
* at send time means the send is refused before any request is made. The
|
|
188
|
+
* signal may span many turns — each send observes its CURRENT state.
|
|
189
|
+
*/
|
|
190
|
+
signal?: AbortSignal;
|
|
144
191
|
}
|
|
145
192
|
|
|
146
193
|
/**
|
|
@@ -159,9 +206,16 @@ export interface UseAgentInvokeOptions {
|
|
|
159
206
|
* - `responding` — assistant text is arriving (`text.start`/`text.delta`
|
|
160
207
|
* silver frames, or bypass text/assistant frames).
|
|
161
208
|
*
|
|
162
|
-
* Failure keeps its own channel ({@link UseAgentInvokeReturn.error}
|
|
163
|
-
* is deliberately no `error`
|
|
164
|
-
* returns to `ready` so the
|
|
209
|
+
* Failure keeps its own channel ({@link UseAgentInvokeReturn.error} +
|
|
210
|
+
* {@link UseAgentInvokeReturn.errorCode}) — there is deliberately no `error`
|
|
211
|
+
* status: after any terminal outcome the status returns to `ready` so the
|
|
212
|
+
* composer re-enables.
|
|
213
|
+
*
|
|
214
|
+
* There is likewise no `retrying` state. `fetchStreamTransport` retries a
|
|
215
|
+
* `POD_SATURATED` refusal once by itself, but that happens before any frame is
|
|
216
|
+
* yielded, so the turn stays in `connecting` for the backoff and the hook never
|
|
217
|
+
* learns it happened. This union describes the POD's turn lifecycle; transport
|
|
218
|
+
* plumbing does not belong in it.
|
|
165
219
|
*/
|
|
166
220
|
export type AgentInvokeStatus = "ready" | "connecting" | "thinking" | "using-tool" | "responding";
|
|
167
221
|
|
|
@@ -174,6 +228,27 @@ export interface UseAgentInvokeReturn {
|
|
|
174
228
|
/** The active tool's wire name while `status === 'using-tool'`, else null. */
|
|
175
229
|
activeTool: string | null;
|
|
176
230
|
error: string | null;
|
|
231
|
+
/**
|
|
232
|
+
* The pod's wire code for the failure in {@link error}, when it carried one
|
|
233
|
+
* — `QUOTA_EXCEEDED`, `POD_SATURATED`, `GUEST_ACCESS_DISABLED`, … (see
|
|
234
|
+
* `AGENT_ERROR_CODES`, and branch on those constants rather than re-typing
|
|
235
|
+
* the literals). `null` when there is no error, or when the failure had no
|
|
236
|
+
* code: a network drop, a host-adapter throw, or an `event: error` frame
|
|
237
|
+
* without one.
|
|
238
|
+
*
|
|
239
|
+
* Both failure channels feed it — the pre-stream refusal (thrown as
|
|
240
|
+
* `AgentResponseError`) and the in-band `event: error` frame — because the
|
|
241
|
+
* two carry the SAME vocabulary; a consumer branches once, not per channel.
|
|
242
|
+
* Set and cleared in lockstep with {@link error}: a new `send()`, `reset()`,
|
|
243
|
+
* or an app switch clears both.
|
|
244
|
+
*
|
|
245
|
+
* It is a `string`, not the `AgentErrorCode` union: the pod may ship a new
|
|
246
|
+
* code before a consumer upgrades this SDK, and a narrowed type would make
|
|
247
|
+
* that unrepresentable rather than merely unhandled. ONE code is
|
|
248
|
+
* client-originated rather than a pod wire code: `STREAM_STALLED` (see
|
|
249
|
+
* `CLIENT_ERROR_CODES`), set when the guuey#192 stall watchdog gives up.
|
|
250
|
+
*/
|
|
251
|
+
errorCode: string | null;
|
|
177
252
|
threadId: string | null;
|
|
178
253
|
/** Abort the in-flight turn (the stream stops; partial text is kept). */
|
|
179
254
|
abort: () => void;
|
|
@@ -232,4 +307,28 @@ export interface UseAgentInvokeReturn {
|
|
|
232
307
|
profileLinkRequest: ProfileLinkRequest | null;
|
|
233
308
|
/** Dismiss the pending {@link profileLinkRequest} (back to `null`). */
|
|
234
309
|
clearProfileLinkRequest: () => void;
|
|
310
|
+
/**
|
|
311
|
+
* The LAST turn ended by user abort (`abort()` or the external signal) —
|
|
312
|
+
* the transcript renderer's R1 aborted-partial + "Stopped." signal.
|
|
313
|
+
* Distinct from the #192 watchdog's internal stream abort, which never
|
|
314
|
+
* sets it. Cleared by the next `send()` and by `reset()`.
|
|
315
|
+
*/
|
|
316
|
+
aborted: boolean;
|
|
317
|
+
/**
|
|
318
|
+
* The LAST turn was ADOPTED from persisted history by the guuey#192 stall
|
|
319
|
+
* watchdog (the reply was already durably written; the dead stream was
|
|
320
|
+
* discarded). Presentation contract: calm surfaces render an adopted turn
|
|
321
|
+
* identically to a streamed one; debug surfaces may mark it. Cleared by
|
|
322
|
+
* the next `send()` and by `reset()`.
|
|
323
|
+
*/
|
|
324
|
+
adopted: boolean;
|
|
325
|
+
/**
|
|
326
|
+
* The optimistic-send lifecycle, keyed by each live user turn's
|
|
327
|
+
* `clientMessageId` (see {@link AgentMessage.clientMessageId}):
|
|
328
|
+
* `"sending"` until the pod's `session` frame admits the turn, `"failed"`
|
|
329
|
+
* when the turn errored BEFORE admission (the message never reached the
|
|
330
|
+
* agent — the R0 failed-to-send state with its retry affordance). Entries
|
|
331
|
+
* for admitted turns are removed (absent = sent). `reset()` clears it.
|
|
332
|
+
*/
|
|
333
|
+
sendStates: Readonly<Record<string, "sending" | "failed">>;
|
|
235
334
|
}
|