@guuey/agent-client 0.4.0 → 0.6.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.
@@ -0,0 +1,90 @@
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 { type ColdStartRetryOptions, type SaturationRetryOptions } from "./saturation-retry.js";
19
+ /**
20
+ * Header carrying a caller-owned anonymous guest secret. A LOCAL MIRROR of the
21
+ * two server-side constants — the pod's `GUEST_HEADER_NAME`
22
+ * (`backend/services/nocode-runtime/src/identity.ts`) and the read plane's
23
+ * `GUEST_HEADER` (`backend/amplify/functions/publicApi/identity.ts`) — because
24
+ * this is a published npm package and cannot take a `@guuey-private` dep (same
25
+ * arrangement as `@guuey/host`'s mirrored fs-contract constants). The string is
26
+ * a wire contract: both planes already advertise it in
27
+ * `Access-Control-Allow-Headers`, so changing it is a breaking protocol change,
28
+ * not a rename.
29
+ */
30
+ export declare const GUEST_HEADER = "x-guuey-guest";
31
+ /**
32
+ * Narrow a caller-supplied guest secret to a value that is safe to put on the
33
+ * wire, or `null`. The single gate for the header: every write of
34
+ * {@link GUEST_HEADER} in this package goes through it, so a malformed secret
35
+ * can never reach a request. The value is never logged (here or anywhere on
36
+ * this path) — it IS the anonymous identity, so a leak is an impersonation.
37
+ */
38
+ export declare function sendableGuestSecret(secret: string | null | undefined): string | null;
39
+ /** Options for {@link fetchStreamTransport}. */
40
+ export interface FetchStreamTransportOptions extends SaturationRetryOptions {
41
+ /**
42
+ * Bounded retry on cold-start 503s — the envelope-less refusal an embed
43
+ * eats for ~30–60s after the agent redeploys (guuey#186 Gap 3). ON by
44
+ * default (small budget: 3 attempts, 2s/4s/8s) for parity with guuey's
45
+ * first-party embeds; pass `false` to disable, or options to re-budget.
46
+ * See {@link withColdStartRetry} for exactly what matches (and what
47
+ * deliberately stays with the saturation policy instead).
48
+ */
49
+ coldStartRetry?: ColdStartRetryOptions | false;
50
+ /**
51
+ * Injectable bearer provider (guuey#186 Gap 4) — identity is a transport
52
+ * concern (see {@link InvokeTransport}: "owns headers + identity
53
+ * entirely"), and a harness or non-React host holds credentials in its own
54
+ * lifecycle, not in a closure minted once at page load. Resolved PER
55
+ * ATTEMPT, before each request — a retry after a backoff wait re-reads it,
56
+ * so a token that expired during the wait is refreshed rather than
57
+ * replayed. When present it takes precedence over the positional
58
+ * `accessToken`; resolving `null` falls through to the guest secret /
59
+ * cookie chain exactly as a null `accessToken` does (and carries the same
60
+ * silent-anonymous-downgrade hazard the `createWebAdapters` docs warn
61
+ * about). A throw propagates and fails the invoke — deliberately not
62
+ * caught, for the same reason as `getGuestSecret` there.
63
+ */
64
+ getBearer?: () => string | null | Promise<string | null>;
65
+ }
66
+ /**
67
+ * The web SSE transport: {@link streamInvokeOnce} under the shared
68
+ * {@link withSaturationRetry} wrapper, itself under {@link withColdStartRetry}.
69
+ * Every consumer of this transport (Studio, the widget, anything built on
70
+ * `createWebAdapters`) therefore inherits the single `POD_SATURATED` retry AND
71
+ * the bounded cold-start 503 retry, the same pair Portal's React-Native
72
+ * transport wears — see the wrappers' docblocks for which refusals retry,
73
+ * which deliberately do not, and why both retries are invisible to the hook.
74
+ * Both wrappers guard on "nothing yielded yet": once a chunk has streamed,
75
+ * NOTHING re-POSTs.
76
+ */
77
+ export declare function fetchStreamTransport(req: InvokeRequest, accessToken?: string | null, guestSecret?: string | null, options?: FetchStreamTransportOptions): AsyncIterable<string>;
78
+ /**
79
+ * Wrap a transport so every yielded chunk ALSO pings `onChunk` — the
80
+ * byte-level liveness signal `useAgentInvoke`'s stall watchdog runs on
81
+ * (guuey#192). Purely observational: chunks pass through unchanged, errors
82
+ * and completion propagate untouched, and the wrapper adds no timers of its
83
+ * own — the OBSERVER owns the clock, this module only reports activity. The
84
+ * first ping doubles as the "first byte seen" arming signal, which is why
85
+ * the watchdog never fires during a silent cold start: no bytes, no ping,
86
+ * no armed timer (that phase belongs to {@link withColdStartRetry} and the
87
+ * user's own abort).
88
+ */
89
+ export declare function withActivityObserver(transport: InvokeTransport, onChunk: () => void): InvokeTransport;
90
+ //# sourceMappingURL=transport.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"transport.d.ts","sourceRoot":"","sources":["../src/transport.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AACH,OAAO,KAAK,EAAE,aAAa,EAAE,eAAe,EAAE,MAAM,YAAY,CAAC;AAEjE,OAAO,EAIL,KAAK,qBAAqB,EAC1B,KAAK,sBAAsB,EAC5B,MAAM,uBAAuB,CAAC;AAE/B;;;;;;;;;;GAUG;AACH,eAAO,MAAM,YAAY,kBAAkB,CAAC;AAgB5C;;;;;;GAMG;AACH,wBAAgB,mBAAmB,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,GAAG,MAAM,GAAG,IAAI,CAEpF;AAoHD,gDAAgD;AAChD,MAAM,WAAW,2BAA4B,SAAQ,sBAAsB;IACzE;;;;;;;OAOG;IACH,cAAc,CAAC,EAAE,qBAAqB,GAAG,KAAK,CAAC;IAC/C;;;;;;;;;;;;;OAaG;IACH,SAAS,CAAC,EAAE,MAAM,MAAM,GAAG,IAAI,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAAC;CAC1D;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,oBAAoB,CAClC,GAAG,EAAE,aAAa,EAClB,WAAW,CAAC,EAAE,MAAM,GAAG,IAAI,EAC3B,WAAW,CAAC,EAAE,MAAM,GAAG,IAAI,EAC3B,OAAO,GAAE,2BAAgC,GACxC,aAAa,CAAC,MAAM,CAAC,CAcvB;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,oBAAoB,CAClC,SAAS,EAAE,eAAe,EAC1B,OAAO,EAAE,MAAM,IAAI,GAClB,eAAe,CAOjB"}
@@ -0,0 +1,189 @@
1
+ import { AgentResponseError } from "./errors.js";
2
+ import { parseRetryAfterSeconds, withColdStartRetry, withSaturationRetry, } from "./saturation-retry.js";
3
+ /**
4
+ * Header carrying a caller-owned anonymous guest secret. A LOCAL MIRROR of the
5
+ * two server-side constants — the pod's `GUEST_HEADER_NAME`
6
+ * (`backend/services/nocode-runtime/src/identity.ts`) and the read plane's
7
+ * `GUEST_HEADER` (`backend/amplify/functions/publicApi/identity.ts`) — because
8
+ * this is a published npm package and cannot take a `@guuey-private` dep (same
9
+ * arrangement as `@guuey/host`'s mirrored fs-contract constants). The string is
10
+ * a wire contract: both planes already advertise it in
11
+ * `Access-Control-Allow-Headers`, so changing it is a breaking protocol change,
12
+ * not a rename.
13
+ */
14
+ export const GUEST_HEADER = "x-guuey-guest";
15
+ /**
16
+ * A well-formed guest secret: exactly 32 bytes as 64 LOWERCASE hex chars —
17
+ * the shape `crypto.getRandomValues` + hex-encoding mints.
18
+ *
19
+ * Deliberately stricter than the server's `/^[a-f0-9]{64}$/i` (pod
20
+ * `identity.ts`, publicApi `identity.ts`): both sides lowercase before
21
+ * hashing, so an uppercase secret would in fact be accepted, but the only
22
+ * supported mint path emits lowercase and a non-canonical value means the
23
+ * caller's storage is not what this adapter expects. Anything that fails is
24
+ * IGNORED — the request falls through to cookie mode rather than sending a
25
+ * secret the two identity planes might key differently.
26
+ */
27
+ const GUEST_SECRET_RE = /^[0-9a-f]{64}$/;
28
+ /**
29
+ * Narrow a caller-supplied guest secret to a value that is safe to put on the
30
+ * wire, or `null`. The single gate for the header: every write of
31
+ * {@link GUEST_HEADER} in this package goes through it, so a malformed secret
32
+ * can never reach a request. The value is never logged (here or anywhere on
33
+ * this path) — it IS the anonymous identity, so a leak is an impersonation.
34
+ */
35
+ export function sendableGuestSecret(secret) {
36
+ return typeof secret === "string" && GUEST_SECRET_RE.test(secret) ? secret : null;
37
+ }
38
+ /**
39
+ * Is this request a cross-origin call from a browser document? Only then can
40
+ * a fetch `TypeError` be a CORS refusal worth hinting about. `location` is
41
+ * read via `typeof` so Node and React Native (where CORS does not exist)
42
+ * answer false; an unparseable URL answers false rather than throwing from
43
+ * inside error handling.
44
+ */
45
+ function isCrossOriginBrowserCall(url) {
46
+ if (typeof location === "undefined")
47
+ return false;
48
+ try {
49
+ return new URL(url).origin !== location.origin;
50
+ }
51
+ catch {
52
+ return false;
53
+ }
54
+ }
55
+ /**
56
+ * One invoke attempt: opens the request and yields decoded SSE chunks.
57
+ * {@link fetchStreamTransport} wraps this with the shared saturation retry —
58
+ * every behaviour below is per-attempt.
59
+ *
60
+ * Exactly ONE identity carrier per request, in order:
61
+ *
62
+ * 1. `accessToken` → `Authorization: Bearer` — the pod identifies the caller
63
+ * by their verified access token (the same identity the history read
64
+ * plane uses, so persisted threads round-trip on reload).
65
+ * 2. a well-formed `guestSecret` → `x-guuey-guest` — the caller owns and
66
+ * persists its own anonymous secret. The path for hosts with no usable
67
+ * cookie jar: React-Native, and the embedded widget, whose third-party
68
+ * iframe cannot rely on the pod's cookie surviving browser partitioning.
69
+ * The pod never mints a cookie for a header client.
70
+ * 3. neither → `credentials: "include"`, which round-trips the HttpOnly
71
+ * `guuey_guest` cookie the pod mints for anonymous browser callers.
72
+ *
73
+ * Never two at once: a bearer wins over a guest secret, and a request that
74
+ * carries either header does NOT also send cookie credentials.
75
+ *
76
+ * Reads the body via `ReadableStream.getReader()` (browser).
77
+ */
78
+ async function* streamInvokeOnce(req, accessToken, guestSecret) {
79
+ const headers = {
80
+ "Content-Type": "application/json",
81
+ Accept: "text/event-stream",
82
+ };
83
+ const init = {
84
+ method: "POST",
85
+ signal: req.signal,
86
+ headers,
87
+ body: JSON.stringify(req.body),
88
+ };
89
+ const guest = sendableGuestSecret(guestSecret);
90
+ if (accessToken) {
91
+ headers.Authorization = `Bearer ${accessToken}`;
92
+ }
93
+ else if (guest) {
94
+ headers[GUEST_HEADER] = guest;
95
+ }
96
+ else {
97
+ init.credentials = "include";
98
+ }
99
+ let resp;
100
+ try {
101
+ resp = await fetch(req.url, init);
102
+ }
103
+ catch (err) {
104
+ // A network-level TypeError on a CROSS-ORIGIN invoke from a browser is,
105
+ // in practice, very often a missing allowedDomains entry — the CORS
106
+ // preflight failed and the web platform deliberately reports nothing
107
+ // more specific (guuey#186 Gap 2: the console.ggui.ai embed lost real
108
+ // time to an unexplained "Failed to fetch"). The error stays a
109
+ // TypeError with the original as `cause`; the added sentence is a HINT,
110
+ // not a diagnosis — offline, DNS and CSP failures throw the same shape.
111
+ // Same-origin calls and non-browser runtimes (no `location`) cannot be
112
+ // CORS refusals, so they pass through untouched.
113
+ if (err instanceof TypeError && isCrossOriginBrowserCall(req.url)) {
114
+ throw new TypeError(`${err.message} — if this is a browser embed, check the app's allowedDomains (the CORS allowlist must include this page's origin)`, { cause: err });
115
+ }
116
+ throw err;
117
+ }
118
+ if (!resp.ok || !resp.body) {
119
+ // Surface a structured pod error ({ code, message }) when present — e.g. a
120
+ // QUOTA_EXCEEDED 429 carries an upgrade message the UI should show. Fall
121
+ // back to the bare status for non-JSON failures.
122
+ const body = await resp.json().catch(() => null);
123
+ let message = `agent responded ${resp.status}`;
124
+ let code;
125
+ if (body !== null && typeof body === "object") {
126
+ if ("message" in body && typeof body.message === "string" && body.message) {
127
+ message = body.message;
128
+ }
129
+ if ("code" in body && typeof body.code === "string") {
130
+ code = body.code;
131
+ }
132
+ }
133
+ throw new AgentResponseError(message, resp.status, code, parseRetryAfterSeconds(resp.headers.get("Retry-After")));
134
+ }
135
+ const reader = resp.body.getReader();
136
+ const decoder = new TextDecoder();
137
+ for (;;) {
138
+ const { value, done } = await reader.read();
139
+ if (done)
140
+ break;
141
+ yield decoder.decode(value, { stream: true });
142
+ }
143
+ }
144
+ /**
145
+ * The web SSE transport: {@link streamInvokeOnce} under the shared
146
+ * {@link withSaturationRetry} wrapper, itself under {@link withColdStartRetry}.
147
+ * Every consumer of this transport (Studio, the widget, anything built on
148
+ * `createWebAdapters`) therefore inherits the single `POD_SATURATED` retry AND
149
+ * the bounded cold-start 503 retry, the same pair Portal's React-Native
150
+ * transport wears — see the wrappers' docblocks for which refusals retry,
151
+ * which deliberately do not, and why both retries are invisible to the hook.
152
+ * Both wrappers guard on "nothing yielded yet": once a chunk has streamed,
153
+ * NOTHING re-POSTs.
154
+ */
155
+ export function fetchStreamTransport(req, accessToken, guestSecret, options = {}) {
156
+ const { getBearer } = options;
157
+ const once = async function* (attempt) {
158
+ // Per-attempt resolution: each retry re-asks the provider (fresh token
159
+ // after a backoff wait) instead of replaying a captured one.
160
+ const bearer = getBearer ? await getBearer() : accessToken;
161
+ yield* streamInvokeOnce(attempt, bearer, guestSecret);
162
+ };
163
+ const saturated = withSaturationRetry(once, { sleep: options.sleep });
164
+ if (options.coldStartRetry === false)
165
+ return saturated(req);
166
+ return withColdStartRetry(saturated, {
167
+ sleep: options.sleep,
168
+ ...options.coldStartRetry,
169
+ })(req);
170
+ }
171
+ /**
172
+ * Wrap a transport so every yielded chunk ALSO pings `onChunk` — the
173
+ * byte-level liveness signal `useAgentInvoke`'s stall watchdog runs on
174
+ * (guuey#192). Purely observational: chunks pass through unchanged, errors
175
+ * and completion propagate untouched, and the wrapper adds no timers of its
176
+ * own — the OBSERVER owns the clock, this module only reports activity. The
177
+ * first ping doubles as the "first byte seen" arming signal, which is why
178
+ * the watchdog never fires during a silent cold start: no bytes, no ping,
179
+ * no armed timer (that phase belongs to {@link withColdStartRetry} and the
180
+ * user's own abort).
181
+ */
182
+ export function withActivityObserver(transport, onChunk) {
183
+ return async function* observed(req) {
184
+ for await (const chunk of transport(req)) {
185
+ onChunk();
186
+ yield chunk;
187
+ }
188
+ };
189
+ }
package/dist/types.d.ts CHANGED
@@ -17,6 +17,13 @@ import type { AgReduceResult, JsonValue } from "@silverprotocol/core";
17
17
  export interface AgentMessage {
18
18
  role: "user" | "assistant";
19
19
  text: string;
20
+ /**
21
+ * The invoke body's `clientMessageId`, present on user turns this client
22
+ * sent live (the optimistic push tags it). History-rehydrated entries and
23
+ * assistant turns omit it. The transcript renderer's R0 send-lifecycle
24
+ * join key (`UseAgentInvokeReturn.sendStates`).
25
+ */
26
+ clientMessageId?: string;
20
27
  }
21
28
  /**
22
29
  * A cross-app profile consent request surfaced mid-stream by the pod's
@@ -117,6 +124,22 @@ export interface AgentInvokeAdapters {
117
124
  /** Optional: rehydrate the transcript for a hydrated threadId. See {@link AgentInvokeHistoryAdapter}. */
118
125
  history?: AgentInvokeHistoryAdapter;
119
126
  }
127
+ /** Tuning for the guuey#192 stall watchdog — see {@link UseAgentInvokeOptions.stallRecovery}. */
128
+ export interface StallRecoveryOptions {
129
+ /**
130
+ * Byte-inactivity window (ms) before a history probe fires. Armed only
131
+ * AFTER the first byte of the turn — pre-first-byte silence is a legitimate
132
+ * cold start and never triggers. Every received chunk resets it.
133
+ * Default 25000.
134
+ */
135
+ windowMs?: number;
136
+ /**
137
+ * Fruitless probes (history shows the turn still in flight, or no probe is
138
+ * possible) before the turn fails with `CLIENT_ERROR_CODES.STREAM_STALLED`.
139
+ * Any received byte resets the count. Default 4.
140
+ */
141
+ probeAttempts?: number;
142
+ }
120
143
  export interface UseAgentInvokeOptions {
121
144
  /** Pod base URL (with or without a trailing `/agent/invoke`). Chat is disabled when null. */
122
145
  endpointUrl: string | null;
@@ -131,6 +154,29 @@ export interface UseAgentInvokeOptions {
131
154
  * reducer is never constructed and the text behaviour is byte-identical.
132
155
  */
133
156
  preserveBlocks?: boolean;
157
+ /**
158
+ * Stall recovery for a half-dead stream (guuey#192). A connection that dies
159
+ * WITHOUT erroring (TCP alive, zero bytes, no `done`) would otherwise leave
160
+ * the turn suspended forever — a frozen cursor while the backend may have
161
+ * completed and persisted the reply. On by default; `false` restores the
162
+ * old hang-forever behaviour; an object tunes the window/attempts. Armed
163
+ * only after the first byte; probes are non-destructive (they read history
164
+ * WITHOUT touching the live stream) and adopt the finished reply from the
165
+ * history adapter when it is already persisted — see the watchdog notes in
166
+ * `useAgentInvoke`.
167
+ */
168
+ stallRecovery?: false | StallRecoveryOptions;
169
+ /**
170
+ * Optional external abort authority (guuey#186 Gap 4): a host lifecycle —
171
+ * route change, dialog close, screen unmount — that must be able to stop
172
+ * an in-flight turn without holding the hook's own `abort()`. Composed
173
+ * with (never replacing) the hook's internal per-turn controller: when
174
+ * this signal aborts, the current turn aborts exactly as `abort()` would;
175
+ * the hook's `abort()`/`reset()` keep working regardless. Already-aborted
176
+ * at send time means the send is refused before any request is made. The
177
+ * signal may span many turns — each send observes its CURRENT state.
178
+ */
179
+ signal?: AbortSignal;
134
180
  }
135
181
  /**
136
182
  * The per-turn lifecycle (guuey#91), derived ENTIRELY from frames the pod
@@ -185,7 +231,9 @@ export interface UseAgentInvokeReturn {
185
231
  *
186
232
  * It is a `string`, not the `AgentErrorCode` union: the pod may ship a new
187
233
  * code before a consumer upgrades this SDK, and a narrowed type would make
188
- * that unrepresentable rather than merely unhandled.
234
+ * that unrepresentable rather than merely unhandled. ONE code is
235
+ * client-originated rather than a pod wire code: `STREAM_STALLED` (see
236
+ * `CLIENT_ERROR_CODES`), set when the guuey#192 stall watchdog gives up.
189
237
  */
190
238
  errorCode: string | null;
191
239
  threadId: string | null;
@@ -246,5 +294,29 @@ export interface UseAgentInvokeReturn {
246
294
  profileLinkRequest: ProfileLinkRequest | null;
247
295
  /** Dismiss the pending {@link profileLinkRequest} (back to `null`). */
248
296
  clearProfileLinkRequest: () => void;
297
+ /**
298
+ * The LAST turn ended by user abort (`abort()` or the external signal) —
299
+ * the transcript renderer's R1 aborted-partial + "Stopped." signal.
300
+ * Distinct from the #192 watchdog's internal stream abort, which never
301
+ * sets it. Cleared by the next `send()` and by `reset()`.
302
+ */
303
+ aborted: boolean;
304
+ /**
305
+ * The LAST turn was ADOPTED from persisted history by the guuey#192 stall
306
+ * watchdog (the reply was already durably written; the dead stream was
307
+ * discarded). Presentation contract: calm surfaces render an adopted turn
308
+ * identically to a streamed one; debug surfaces may mark it. Cleared by
309
+ * the next `send()` and by `reset()`.
310
+ */
311
+ adopted: boolean;
312
+ /**
313
+ * The optimistic-send lifecycle, keyed by each live user turn's
314
+ * `clientMessageId` (see {@link AgentMessage.clientMessageId}):
315
+ * `"sending"` until the pod's `session` frame admits the turn, `"failed"`
316
+ * when the turn errored BEFORE admission (the message never reached the
317
+ * agent — the R0 failed-to-send state with its retry affordance). Entries
318
+ * for admitted turns are removed (absent = sent). `reset()` clears it.
319
+ */
320
+ sendStates: Readonly<Record<string, "sending" | "failed">>;
249
321
  }
250
322
  //# sourceMappingURL=types.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAEH,OAAO,KAAK,EAAE,cAAc,EAAE,SAAS,EAAE,MAAM,sBAAsB,CAAC;AAEtE,uDAAuD;AACvD,MAAM,WAAW,YAAY;IAC3B,IAAI,EAAE,MAAM,GAAG,WAAW,CAAC;IAC3B,IAAI,EAAE,MAAM,CAAC;CACd;AAED;;;;;;;;GAQG;AACH,MAAM,WAAW,qBAAqB;IACpC,KAAK,EAAE,MAAM,CAAC;IACd,SAAS,EAAE,MAAM,GAAG,YAAY,CAAC;CAClC;AAED;;;;;;;;;GASG;AACH,MAAM,WAAW,kBAAkB;IACjC,KAAK,EAAE,MAAM,CAAC;IACd,SAAS,EAAE,MAAM,GAAG,YAAY,CAAC;CAClC;AAED;;;;;;;GAOG;AACH,MAAM,WAAW,WAAW;IAC1B,GAAG,EAAE,MAAM,CAAC;IACZ,EAAE,EAAE,MAAM,CAAC;IACX,YAAY,EAAE,SAAS,CAAC;CACzB;AAED;;;GAGG;AACH,MAAM,WAAW,aAAa;IAC5B,IAAI,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAAC;IAC1D,IAAI,CAAC,GAAG,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;CAC3D;AAED,0EAA0E;AAC1E,MAAM,MAAM,UAAU,GAAG,MAAM,MAAM,CAAC;AAEtC,kDAAkD;AAClD,MAAM,WAAW,aAAa;IAC5B,iFAAiF;IACjF,GAAG,EAAE,MAAM,CAAC;IACZ,kEAAkE;IAClE,IAAI,EAAE,OAAO,CAAC;IACd,mCAAmC;IACnC,MAAM,EAAE,WAAW,CAAC;CACrB;AAED;;;;;GAKG;AACH,MAAM,MAAM,eAAe,GAAG,CAAC,GAAG,EAAE,aAAa,KAAK,aAAa,CAAC,MAAM,CAAC,CAAC;AAE5E;;;;;;;;GAQG;AACH,MAAM,MAAM,iBAAiB,GACzB;IAAE,QAAQ,EAAE,YAAY,EAAE,CAAC;IAAC,KAAK,CAAC,EAAE,WAAW,EAAE,CAAA;CAAE,GACnD;IAAE,IAAI,EAAE,IAAI,CAAA;CAAE,CAAC;AAEnB;;;;;GAKG;AACH,MAAM,WAAW,yBAAyB;IACxC,sFAAsF;IACtF,IAAI,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,iBAAiB,CAAC,CAAC;CACpD;AAED,mEAAmE;AACnE,MAAM,WAAW,mBAAmB;IAClC,OAAO,EAAE,aAAa,CAAC;IACvB,UAAU,EAAE,UAAU,CAAC;IACvB,SAAS,EAAE,eAAe,CAAC;IAC3B,yGAAyG;IACzG,OAAO,CAAC,EAAE,yBAAyB,CAAC;CACrC;AAED,MAAM,WAAW,qBAAqB;IACpC,6FAA6F;IAC7F,WAAW,EAAE,MAAM,GAAG,IAAI,CAAC;IAC3B,yDAAyD;IACzD,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,8DAA8D;IAC9D,QAAQ,EAAE,mBAAmB,CAAC;IAC9B;;;;;OAKG;IACH,cAAc,CAAC,EAAE,OAAO,CAAC;CAC1B;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AACH,MAAM,MAAM,iBAAiB,GAAG,OAAO,GAAG,YAAY,GAAG,UAAU,GAAG,YAAY,GAAG,YAAY,CAAC;AAElG,MAAM,WAAW,oBAAoB;IACnC,QAAQ,EAAE,YAAY,EAAE,CAAC;IACzB,IAAI,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IACvC;mFAC+E;IAC/E,MAAM,EAAE,iBAAiB,CAAC;IAC1B,8EAA8E;IAC9E,UAAU,EAAE,MAAM,GAAG,IAAI,CAAC;IAC1B,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;IACrB;;;;;;;;;;;;;;;;;OAiBG;IACH,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;IACzB,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAC;IACxB,yEAAyE;IACzE,KAAK,EAAE,MAAM,IAAI,CAAC;IAClB,KAAK,EAAE,MAAM,IAAI,CAAC;IAClB;;;;;;;;;;;;;;;;;;OAkBG;IACH,YAAY,EAAE,cAAc,GAAG,IAAI,CAAC;IACpC;;;;;;;;OAQG;IACH,YAAY,EAAE,WAAW,EAAE,CAAC;IAC5B;;;;;;;OAOG;IACH,qBAAqB,EAAE,qBAAqB,GAAG,IAAI,CAAC;IACpD,0EAA0E;IAC1E,0BAA0B,EAAE,MAAM,IAAI,CAAC;IACvC;;;;;;;;;OASG;IACH,kBAAkB,EAAE,kBAAkB,GAAG,IAAI,CAAC;IAC9C,uEAAuE;IACvE,uBAAuB,EAAE,MAAM,IAAI,CAAC;CACrC"}
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"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"}
@@ -16,5 +16,33 @@ export type HistoryApplication = {
16
16
  * mid-flight send always beats late-arriving history.
17
17
  */
18
18
  export declare function applyHistoryResult(result: HistoryLoadResult, currentMessages: AgentMessage[]): HistoryApplication;
19
+ /** The guuey#192 stall watchdog's resolved tuning (see {@link stallProbeDecision}). */
20
+ export declare const STALL_RECOVERY_DEFAULTS: {
21
+ readonly windowMs: 25000;
22
+ readonly probeAttempts: 4;
23
+ };
24
+ /**
25
+ * Pure decision seam for the guuey#192 stall probe: does a freshly-loaded
26
+ * transcript already contain THIS turn's finished reply?
27
+ *
28
+ * `adopt` requires BOTH signals, because each alone lies in a real case:
29
+ *
30
+ * - **user-count**: history must hold at least as many user turns as the
31
+ * local transcript (which includes the just-sent optimistic one). Without
32
+ * it, a thread whose PREVIOUS turn ended in a completed assistant reply
33
+ * would adopt that OLD transcript and silently drop the in-flight turn.
34
+ * - **finished tail**: history's last message must be a non-empty assistant
35
+ * reply. Without it, a history read that caught the persisted user row
36
+ * before the assistant row would adopt a reply-less transcript.
37
+ *
38
+ * KNOWN LIMIT (documented, accepted): the runtime persists a turn's rows at
39
+ * completion — the guuey#192 evidence (a reload mid-stall renders the FULL
40
+ * reply) is only possible under that model, and the read plane carries no
41
+ * per-row clientMessageId to match against. If persistence ever becomes
42
+ * progressive (partial assistant rows), this heuristic needs the read plane
43
+ * to grow a turn-completion marker — do not "fix" it client-side by text
44
+ * comparison, which cannot distinguish a partial row from a finished one.
45
+ */
46
+ export declare function stallProbeDecision(history: AgentMessage[], localUserCount: number): "adopt" | "in-flight";
19
47
  export declare function useAgentInvoke(opts: UseAgentInvokeOptions): UseAgentInvokeReturn;
20
48
  //# sourceMappingURL=useAgentInvoke.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"useAgentInvoke.d.ts","sourceRoot":"","sources":["../src/useAgentInvoke.ts"],"names":[],"mappings":"AAkCA,OAAO,KAAK,EAGV,YAAY,EAEZ,iBAAiB,EAGjB,qBAAqB,EACrB,oBAAoB,EACrB,MAAM,YAAY,CAAC;AAMpB,yEAAyE;AACzE,MAAM,MAAM,kBAAkB,GAC1B;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,YAAY,EAAE,CAAA;CAAE,GAC1C;IAAE,IAAI,EAAE,MAAM,CAAA;CAAE,GAChB;IAAE,IAAI,EAAE,OAAO,CAAA;CAAE,CAAC;AAEtB;;;;;;GAMG;AACH,wBAAgB,kBAAkB,CAChC,MAAM,EAAE,iBAAiB,EACzB,eAAe,EAAE,YAAY,EAAE,GAC9B,kBAAkB,CAIpB;AAED,wBAAgB,cAAc,CAAC,IAAI,EAAE,qBAAqB,GAAG,oBAAoB,CAwVhF"}
1
+ {"version":3,"file":"useAgentInvoke.d.ts","sourceRoot":"","sources":["../src/useAgentInvoke.ts"],"names":[],"mappings":"AA6BA,OAAO,KAAK,EAGV,YAAY,EAEZ,iBAAiB,EAIjB,qBAAqB,EACrB,oBAAoB,EACrB,MAAM,YAAY,CAAC;AAMpB,yEAAyE;AACzE,MAAM,MAAM,kBAAkB,GAC1B;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,YAAY,EAAE,CAAA;CAAE,GAC1C;IAAE,IAAI,EAAE,MAAM,CAAA;CAAE,GAChB;IAAE,IAAI,EAAE,OAAO,CAAA;CAAE,CAAC;AAEtB;;;;;;GAMG;AACH,wBAAgB,kBAAkB,CAChC,MAAM,EAAE,iBAAiB,EACzB,eAAe,EAAE,YAAY,EAAE,GAC9B,kBAAkB,CAIpB;AAED,uFAAuF;AACvF,eAAO,MAAM,uBAAuB;;;CAAkD,CAAC;AAYvF;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,wBAAgB,kBAAkB,CAChC,OAAO,EAAE,YAAY,EAAE,EACvB,cAAc,EAAE,MAAM,GACrB,OAAO,GAAG,WAAW,CAOvB;AAED,wBAAgB,cAAc,CAAC,IAAI,EAAE,qBAAqB,GAAG,oBAAoB,CAmehF"}