@invergent/website-widget 1.4.3

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,406 @@
1
+ import { AbstractAgent, AgentConfig, RunAgentInput, BaseEvent } from '@ag-ui/client';
2
+ export { AbstractAgent, EventType } from '@ag-ui/client';
3
+ import { Observable } from 'rxjs';
4
+
5
+ /**
6
+ * HTTP + SSE wire layer.
7
+ *
8
+ * Pure functions that talk to the website channel's three endpoints,
9
+ * isolated here so :class:`WebsiteAgentClient` can stay focused on
10
+ * state-machine orchestration. All fetches use ``credentials:
11
+ * 'include'`` so the browser sends the HttpOnly session cookie that
12
+ * ``bootstrap`` set -- without it the message/SSE/end routes return
13
+ * 401 and the whole channel falls apart.
14
+ *
15
+ * SSE is handled by the native ``EventSource`` rather than a custom
16
+ * fetch-based parser because the native implementation is zero bytes,
17
+ * auto-reconnects on network drops, and respects browser-internal
18
+ * backoff heuristics. The one thing it can't do is resume with a
19
+ * server-side cursor, so we manage the ``?after=N`` reopen logic
20
+ * explicitly in :class:`WebsiteAgentClient`.
21
+ */
22
+ interface BootstrapResult {
23
+ sessionId: string;
24
+ csrfToken: string;
25
+ expiresAt: number;
26
+ agentName: string;
27
+ }
28
+
29
+ /**
30
+ * AG-UI ``AbstractAgent`` implementation for the Surogates public-website channel.
31
+ *
32
+ * A ``WebsiteAgent`` bridges the channel-specific HTTP protocol
33
+ * (publishable-key bootstrap, HttpOnly cookie, CSRF double-submit,
34
+ * ``/v1/website/sessions/{id}/events`` SSE) onto AG-UI's standard
35
+ * ``run()`` contract so any AG-UI-aware host app -- CopilotKit,
36
+ * custom chat UIs, LangGraph-style clients -- can talk to a Surogates
37
+ * backend with no custom glue beyond instantiation.
38
+ *
39
+ * Responsibilities
40
+ * ----------------
41
+ * * **Lazy bootstrap.** The first ``runAgent`` call POSTs to
42
+ * ``/v1/website/sessions`` to exchange the publishable key for a
43
+ * session cookie + CSRF token; subsequent calls reuse them. A cookie
44
+ * expiry (HTTP 401) triggers a transparent re-bootstrap so the host
45
+ * app never has to manage auth state.
46
+ *
47
+ * * **Message → POST.** On each ``runAgent`` call the agent finds the
48
+ * latest user message in ``input.messages`` and posts its content to
49
+ * ``/v1/website/sessions/{id}/messages`` with the CSRF header.
50
+ *
51
+ * * **SSE → AG-UI.** The channel's native event stream is converted
52
+ * to AG-UI events by :class:`Translator` frame-by-frame. The agent
53
+ * opens a fresh EventSource per run (with ``?after=<cursor>`` to
54
+ * skip already-processed frames) and closes it after a short grace
55
+ * drain on turn completion so trailing server frames (late
56
+ * ``memory.update``, ``session.done``) advance the cursor rather
57
+ * than replaying on the next run.
58
+ *
59
+ * * **RUN lifecycle.** AG-UI requires every run to be bracketed by
60
+ * ``RUN_STARTED`` and exactly one of ``RUN_FINISHED`` / ``RUN_ERROR``.
61
+ * Surogates has no such sentinel on the wire, so the agent emits
62
+ * them itself; a per-run ``terminated`` flag guarantees at most one
63
+ * terminal event even under concurrent SSE ``onerror`` + translator
64
+ * end-of-turn detection.
65
+ */
66
+
67
+ /**
68
+ * Constructor config for :class:`WebsiteAgent`. Extends AG-UI's
69
+ * base ``AgentConfig`` with the two Surogates-specific fields the
70
+ * bootstrap needs; everything else (``agentId``, ``threadId``,
71
+ * ``initialMessages``, ``initialState``, ``debug``) is inherited
72
+ * unchanged so consumers reuse the AG-UI patterns they already know.
73
+ */
74
+ interface WebsiteAgentConfig extends AgentConfig {
75
+ /** Base URL of the Surogates API, e.g. ``https://agent.acme.com``. */
76
+ apiUrl: string;
77
+ /** Publishable key issued by ops (``surg_wk_...``). */
78
+ publishableKey: string;
79
+ }
80
+ declare class WebsiteAgent extends AbstractAgent {
81
+ readonly apiUrl: string;
82
+ readonly publishableKey: string;
83
+ /** Cached bootstrap result. ``undefined`` until the first ``run()``. */
84
+ private bootstrapResult;
85
+ /** Monotonic cursor across runs. Passed to the SSE ``?after=`` param. */
86
+ private cursor;
87
+ constructor(config: WebsiteAgentConfig);
88
+ /**
89
+ * Ensure we have a live session cookie + CSRF token.
90
+ *
91
+ * Idempotent; returns the cached result when one already exists.
92
+ * ``bootstrap()`` is the only call that requires the publishable
93
+ * key to travel in an Authorization header, so callers that want
94
+ * to eagerly validate their configuration (e.g. to surface a
95
+ * setup error at widget-load time) can invoke this directly.
96
+ */
97
+ ensureBootstrapped(): Promise<BootstrapResult>;
98
+ /**
99
+ * Close the current session on the server side and drop local state.
100
+ *
101
+ * Callers should invoke this when the visitor closes the chat UI
102
+ * so the server can mark the session complete instead of waiting
103
+ * for the idle-reset timer. Safe to call when no session has been
104
+ * bootstrapped yet.
105
+ */
106
+ end(): Promise<void>;
107
+ /**
108
+ * Implementation of the AG-UI ``run()`` contract.
109
+ *
110
+ * Returns a cold observable: one subscription per run. AG-UI's
111
+ * ``runAgent`` pipeline pipes this through chunk transformation,
112
+ * verification, and subscriber notifications before updating
113
+ * ``this.messages``.
114
+ */
115
+ run(input: RunAgentInput): Observable<BaseEvent>;
116
+ /**
117
+ * Async body of the observable factory.
118
+ *
119
+ * Emits ``RUN_STARTED`` up front so every downstream failure is
120
+ * well-bracketed by the AG-UI lifecycle. ``RUN_FINISHED`` is
121
+ * emitted by the SSE listeners when the translator detects end-of-
122
+ * turn; ``RUN_ERROR`` is emitted from the catch below when bootstrap
123
+ * or message-send fails before the SSE has any chance to close out.
124
+ *
125
+ * The ``ctx.terminated`` guard is the single enforcement point for
126
+ * AG-UI's "exactly one terminal event" invariant. Every emission
127
+ * site checks it before calling ``subscriber.next`` with RUN_FINISHED
128
+ * or RUN_ERROR, and every terminal call sets it to true first.
129
+ */
130
+ private driveRun;
131
+ /**
132
+ * Tear down whatever EventSource is currently live on ``ctx`` and
133
+ * open a fresh one against *sessionId* starting at *after*. Called
134
+ * both for the initial stream and after cookie recovery. Always
135
+ * clears the stale ``onerror`` first so the old source can't race
136
+ * a ``RUN_ERROR`` into the subscriber after it's been replaced.
137
+ */
138
+ private replaceStreamSource;
139
+ /**
140
+ * Try ``sendMessage``; on auth failure re-bootstrap once and retry.
141
+ *
142
+ * The re-bootstrap mints a new session id, so the caller passes an
143
+ * ``onRebootstrap`` callback that reopens the SSE stream against the
144
+ * fresh session before the retry. Visitor continuity across
145
+ * bootstraps is a deliberate non-feature for v1; the new session
146
+ * starts empty. Only frames emitted on the OLD session between
147
+ * SSE-open and send-failure are lost -- and those are always
148
+ * previous-turn events, since the current turn's ``sendMessage`` is
149
+ * what 401-ed.
150
+ */
151
+ private sendWithCookieRecovery;
152
+ /**
153
+ * Wire up the EventSource to translate and forward every frame,
154
+ * and terminate the observable when the translator flags
155
+ * end-of-turn or a transport failure occurs.
156
+ *
157
+ * After the translator flips end-of-turn we emit ``RUN_FINISHED``
158
+ * immediately, then keep the stream open for ``DRAIN_WINDOW_MS``
159
+ * so trailing frames (late ``memory.update``, the sentinel
160
+ * ``session.done``) advance ``this.cursor`` rather than arriving
161
+ * on the next run. The ``ctx.terminated`` gate stops additional
162
+ * subscriber emissions during the drain.
163
+ */
164
+ private attachStreamListeners;
165
+ /**
166
+ * Find the last user-authored message body to post. AG-UI supports
167
+ * multimodal content arrays; this v1 only handles string content
168
+ * (the website channel's ``POST /messages`` endpoint accepts plain
169
+ * text). Multimodal support lands once the server grows a media
170
+ * upload path.
171
+ */
172
+ private extractUserText;
173
+ }
174
+
175
+ /**
176
+ * Pure event translation: Surogates wire events → AG-UI ``BaseEvent``\ s.
177
+ *
178
+ * The SSE stream delivered by ``/v1/website/sessions/{id}/events`` carries
179
+ * event types defined in ``surogates/session/events.py``
180
+ * (``user.message``, ``llm.delta``, ``llm.response``, ``tool.call``, ...).
181
+ * AG-UI consumers expect a different, standardised vocabulary
182
+ * (``TEXT_MESSAGE_START`` / ``CONTENT`` / ``END`` triads, ``TOOL_CALL_*``,
183
+ * ``RUN_STARTED`` / ``RUN_FINISHED``, ``STATE_SNAPSHOT``, etc.). This
184
+ * module is the contract.
185
+ *
186
+ * Design notes
187
+ * ------------
188
+ * * **Pure and state-carried.** ``Translator`` is a class only because
189
+ * a few decisions need a tiny bit of memory (have we emitted the
190
+ * running ``TextMessageStart`` yet? how many tool calls are
191
+ * outstanding? did the last ``llm.response`` signal end-of-turn?).
192
+ * It does not touch the network or any globals; every instance is a
193
+ * single run's translation state and discardable after
194
+ * ``RUN_FINISHED``.
195
+ *
196
+ * * **Chunks over triads.** AG-UI provides
197
+ * ``TEXT_MESSAGE_CHUNK`` / ``TOOL_CALL_CHUNK`` convenience events
198
+ * that expand to the START / CONTENT / END triad automatically via
199
+ * the client-side ``transformChunks`` pipeline. Surogates already
200
+ * streams in chunks (``llm.delta``, full-payload ``tool.call``), so
201
+ * emitting chunk events keeps the translator short and matches the
202
+ * data we actually have.
203
+ *
204
+ * * **Unknown → CUSTOM.** Platform-specific events we cannot map to
205
+ * first-class AG-UI events (memory updates, saga step transitions,
206
+ * session resets, context compaction) are emitted as AG-UI ``CUSTOM``
207
+ * events with ``name`` set to the Surogates event type and ``value``
208
+ * set to the raw payload. Consumers that care can match on
209
+ * ``name``; consumers that don't simply ignore CUSTOM.
210
+ *
211
+ * * **End-of-turn heuristic.** Our server does not emit a
212
+ * ``turn.complete`` event. Per-turn completion is inferred: an
213
+ * ``llm.response`` whose payload indicates the model is done (no
214
+ * pending tool calls, ``finish_reason`` of ``stop`` / ``end_turn``)
215
+ * flips ``isTurnComplete`` to ``true``. The caller emits
216
+ * ``RUN_FINISHED`` and closes the stream. A ``session.done`` SSE
217
+ * sentinel also completes the turn.
218
+ */
219
+
220
+ /** Minimal shape of a single SSE frame we read from the server. */
221
+ interface SurogatesFrame {
222
+ /** Monotonic server-side event id (cursor for reconnect). */
223
+ id: number;
224
+ /** Surogates event type (``user.message``, ``llm.delta``, etc.). */
225
+ type: string;
226
+ /** Parsed JSON payload from the ``data:`` line. */
227
+ data: unknown;
228
+ }
229
+ /**
230
+ * Stateful translator for one run.
231
+ *
232
+ * The caller is expected to build a fresh instance per ``runAgent`` turn
233
+ * and discard it after the run finishes; every method is idempotent and
234
+ * thread-safe only in the single-SSE-consumer sense (JavaScript events
235
+ * are single-threaded).
236
+ */
237
+ declare class Translator {
238
+ /**
239
+ * Running message id for the current streaming assistant message.
240
+ * Re-used across ``llm.delta`` frames so every chunk targets the
241
+ * same AG-UI message -- that's what makes the client-side
242
+ * ``transformChunks`` stitch them into one logical message.
243
+ */
244
+ private currentMessageId;
245
+ /**
246
+ * Running message id for the current reasoning stream, if any.
247
+ * Kept separate from ``currentMessageId`` so reasoning deltas never
248
+ * accidentally merge into the assistant content stream.
249
+ */
250
+ private currentReasoningId;
251
+ /**
252
+ * Tool calls that have been announced (``TOOL_CALL_CHUNK`` emitted)
253
+ * but have not yet received a matching ``tool.result``. Non-empty
254
+ * means the turn is mid-execution even if an ``llm.response`` looks
255
+ * final.
256
+ */
257
+ private pendingToolCalls;
258
+ /** Set once we've seen an end-of-turn signal. */
259
+ private _turnComplete;
260
+ get isTurnComplete(): boolean;
261
+ /**
262
+ * Translate one SSE frame into zero or more AG-UI events.
263
+ *
264
+ * Returns an empty array when the frame carries no user-visible
265
+ * information (internal bookkeeping events like ``harness.wake``).
266
+ * Returns multiple events when a single Surogates event corresponds
267
+ * to a chunk triad that the AG-UI client can't reconstruct on its
268
+ * own -- though we try to use chunk events to keep this to one
269
+ * output event per input in the common path.
270
+ */
271
+ translate(frame: SurogatesFrame): BaseEvent[];
272
+ private handleLlmDelta;
273
+ private handleLlmResponse;
274
+ private handleLlmThinking;
275
+ private handleToolCall;
276
+ private handleToolResult;
277
+ private handlePolicyDenied;
278
+ private handleExpertDelegation;
279
+ private handleExpertResult;
280
+ private handleTerminalFailure;
281
+ private asCustom;
282
+ /**
283
+ * Generate a stable message id derived from the triggering frame's
284
+ * event id. The event id is globally unique per session, so using
285
+ * it as a suffix guarantees uniqueness without pulling in ``uuid``.
286
+ */
287
+ private mintMessageId;
288
+ }
289
+
290
+ /**
291
+ * Error taxonomy for the website-widget SDK.
292
+ *
293
+ * Consumers should not have to parse strings to tell "the key is wrong"
294
+ * apart from "the network blinked" -- different remediations apply.
295
+ * Every error thrown or emitted by the SDK derives from
296
+ * :class:`SurogatesError` so a single ``instanceof`` catch is always
297
+ * safe; the subclasses encode the specific failure mode.
298
+ */
299
+ declare class SurogatesError extends Error {
300
+ /** HTTP status code when the error originated from a server response. */
301
+ readonly status?: number;
302
+ /** Raw server detail string, if any. */
303
+ readonly detail?: string;
304
+ constructor(message: string, opts?: {
305
+ status?: number;
306
+ detail?: string;
307
+ cause?: unknown;
308
+ });
309
+ }
310
+ /**
311
+ * The publishable key is invalid, expired, or the request Origin is not
312
+ * in the agent's allow-list. Non-retryable: the embed is misconfigured
313
+ * and no amount of backoff will help.
314
+ */
315
+ declare class SurogatesAuthError extends SurogatesError {
316
+ constructor(message: string, opts?: {
317
+ status?: number;
318
+ detail?: string;
319
+ });
320
+ }
321
+ /**
322
+ * Rate-limited (HTTP 429) or the per-session message/token cap has
323
+ * been reached. Retryable after a backoff; the SDK exposes this as a
324
+ * distinct type so consumers can show a "slow down" indicator instead
325
+ * of a generic failure.
326
+ */
327
+ declare class SurogatesRateLimitError extends SurogatesError {
328
+ /** Seconds the server asked us to wait, if it included a Retry-After header. */
329
+ readonly retryAfter?: number;
330
+ constructor(message: string, opts?: {
331
+ status?: number;
332
+ retryAfter?: number;
333
+ detail?: string;
334
+ });
335
+ }
336
+ /**
337
+ * The protocol itself is the problem -- the SDK received a response it
338
+ * can't interpret, or a cookie/CSRF invariant was violated. Indicates
339
+ * an SDK-server version mismatch or a corrupt response; raises the
340
+ * priority of whatever error reporting channel the host app has.
341
+ */
342
+ declare class SurogatesProtocolError extends SurogatesError {
343
+ constructor(message: string, opts?: {
344
+ status?: number;
345
+ detail?: string;
346
+ cause?: unknown;
347
+ });
348
+ }
349
+ /**
350
+ * Transport-level failure: network drop, CORS preflight refusal, DNS,
351
+ * browser offline, 5xx from an ingress/load balancer. Retryable by
352
+ * design; the SDK does not auto-retry today -- the caller should back
353
+ * off and re-issue the operation (typically by re-invoking
354
+ * ``runAgent``). A future revision may add internal exponential
355
+ * backoff; consumers should continue to surface these errors to the
356
+ * user rather than assume they'll be swallowed.
357
+ */
358
+ declare class SurogatesNetworkError extends SurogatesError {
359
+ constructor(message: string, opts?: {
360
+ cause?: unknown;
361
+ });
362
+ }
363
+
364
+ /**
365
+ * Wire-protocol and SDK-version constants.
366
+ *
367
+ * ``PROTOCOL_VERSION`` is the integer the SDK sends to the server so
368
+ * future breaking protocol changes can be negotiated or hard-failed at
369
+ * bootstrap instead of silently corrupting running embeds.
370
+ *
371
+ * ``SDK_VERSION`` travels in an ``X-Surogates-Widget-Version`` header on
372
+ * every request so server logs can correlate a buggy client build with
373
+ * its error surface. Kept in sync with ``package.json`` manually; the
374
+ * build pipeline can replace it at bundle time in a later phase.
375
+ */
376
+ declare const PROTOCOL_VERSION = 1;
377
+ declare const SDK_VERSION = "0.1.0";
378
+ declare const SURG_EVENT: {
379
+ readonly USER_MESSAGE: "user.message";
380
+ readonly LLM_REQUEST: "llm.request";
381
+ readonly LLM_RESPONSE: "llm.response";
382
+ readonly LLM_THINKING: "llm.thinking";
383
+ readonly LLM_DELTA: "llm.delta";
384
+ readonly TOOL_CALL: "tool.call";
385
+ readonly TOOL_RESULT: "tool.result";
386
+ readonly SANDBOX_PROVISION: "sandbox.provision";
387
+ readonly SANDBOX_EXECUTE: "sandbox.execute";
388
+ readonly SANDBOX_RESULT: "sandbox.result";
389
+ readonly SANDBOX_DESTROY: "sandbox.destroy";
390
+ readonly SESSION_START: "session.start";
391
+ readonly SESSION_PAUSE: "session.pause";
392
+ readonly SESSION_RESUME: "session.resume";
393
+ readonly SESSION_COMPLETE: "session.complete";
394
+ readonly SESSION_FAIL: "session.fail";
395
+ readonly SESSION_DONE: "session.done";
396
+ readonly CONTEXT_COMPACT: "context.compact";
397
+ readonly MEMORY_UPDATE: "memory.update";
398
+ readonly EXPERT_DELEGATION: "expert.delegation";
399
+ readonly EXPERT_RESULT: "expert.result";
400
+ readonly EXPERT_FAILURE: "expert.failure";
401
+ readonly POLICY_DENIED: "policy.denied";
402
+ readonly POLICY_ALLOWED: "policy.allowed";
403
+ readonly HARNESS_CRASH: "harness.crash";
404
+ };
405
+
406
+ export { PROTOCOL_VERSION, SDK_VERSION, SURG_EVENT, SurogatesAuthError, SurogatesError, type SurogatesFrame, SurogatesNetworkError, SurogatesProtocolError, SurogatesRateLimitError, Translator, WebsiteAgent, type WebsiteAgentConfig };