@invergent/website-widget 2.7.5 → 2.7.6

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,700 @@
1
+ import { EventType, AbstractAgent, AGUIError } from '@ag-ui/client';
2
+ import { Observable } from 'rxjs';
3
+
4
+ // src/constants.ts
5
+ var PROTOCOL_VERSION = 1;
6
+ var SDK_VERSION = "0.1.0";
7
+ var CSRF_HEADER = "X-CSRF-Token";
8
+ var VERSION_HEADER = "X-Surogates-Widget-Version";
9
+ var PATH_BOOTSTRAP = "/v1/website/sessions";
10
+ var PATH_MESSAGES = (sessionId) => `/v1/website/sessions/${sessionId}/messages`;
11
+ var PATH_EVENTS = (sessionId, after) => `/v1/website/sessions/${sessionId}/events?after=${after}`;
12
+ var PATH_END = (sessionId) => `/v1/website/sessions/${sessionId}/end`;
13
+ var SURG_EVENT = {
14
+ USER_MESSAGE: "user.message",
15
+ LLM_REQUEST: "llm.request",
16
+ LLM_RESPONSE: "llm.response",
17
+ LLM_THINKING: "llm.thinking",
18
+ LLM_DELTA: "llm.delta",
19
+ TOOL_CALL: "tool.call",
20
+ TOOL_RESULT: "tool.result",
21
+ SANDBOX_PROVISION: "sandbox.provision",
22
+ SANDBOX_EXECUTE: "sandbox.execute",
23
+ SANDBOX_RESULT: "sandbox.result",
24
+ SANDBOX_DESTROY: "sandbox.destroy",
25
+ SESSION_START: "session.start",
26
+ SESSION_PAUSE: "session.pause",
27
+ SESSION_RESUME: "session.resume",
28
+ SESSION_COMPLETE: "session.complete",
29
+ SESSION_FAIL: "session.fail",
30
+ SESSION_DONE: "session.done",
31
+ CONTEXT_COMPACT: "context.compact",
32
+ MEMORY_UPDATE: "memory.update",
33
+ EXPERT_DELEGATION: "expert.delegation",
34
+ EXPERT_RESULT: "expert.result",
35
+ EXPERT_FAILURE: "expert.failure",
36
+ POLICY_DENIED: "policy.denied",
37
+ POLICY_ALLOWED: "policy.allowed",
38
+ HARNESS_CRASH: "harness.crash"
39
+ };
40
+
41
+ // src/errors.ts
42
+ var SurogatesError = class extends Error {
43
+ constructor(message, opts) {
44
+ super(message);
45
+ this.name = "SurogatesError";
46
+ if (opts?.status !== void 0) this.status = opts.status;
47
+ if (opts?.detail !== void 0) this.detail = opts.detail;
48
+ if (opts?.cause !== void 0) this.cause = opts.cause;
49
+ }
50
+ };
51
+ var SurogatesAuthError = class extends SurogatesError {
52
+ constructor(message, opts) {
53
+ super(message, opts);
54
+ this.name = "SurogatesAuthError";
55
+ }
56
+ };
57
+ var SurogatesRateLimitError = class extends SurogatesError {
58
+ constructor(message, opts) {
59
+ super(message, opts);
60
+ this.name = "SurogatesRateLimitError";
61
+ if (opts?.retryAfter !== void 0) this.retryAfter = opts.retryAfter;
62
+ }
63
+ };
64
+ var SurogatesProtocolError = class extends SurogatesError {
65
+ constructor(message, opts) {
66
+ super(message, opts);
67
+ this.name = "SurogatesProtocolError";
68
+ }
69
+ };
70
+ var SurogatesNetworkError = class extends SurogatesError {
71
+ constructor(message, opts) {
72
+ super(message, opts);
73
+ this.name = "SurogatesNetworkError";
74
+ }
75
+ };
76
+ var END_OF_TURN_FINISH_REASONS = /* @__PURE__ */ new Set(["stop", "end_turn", "length"]);
77
+ var CONTINUATION_FINISH_REASONS = /* @__PURE__ */ new Set(["tool_calls", "function_call", "tool_use"]);
78
+ var Translator = class {
79
+ constructor() {
80
+ /**
81
+ * Tool calls that have been announced (``TOOL_CALL_CHUNK`` emitted)
82
+ * but have not yet received a matching ``tool.result``. Non-empty
83
+ * means the turn is mid-execution even if an ``llm.response`` looks
84
+ * final.
85
+ */
86
+ this.pendingToolCalls = /* @__PURE__ */ new Set();
87
+ /** Set once we've seen an end-of-turn signal. */
88
+ this._turnComplete = false;
89
+ }
90
+ get isTurnComplete() {
91
+ return this._turnComplete;
92
+ }
93
+ /**
94
+ * Translate one SSE frame into zero or more AG-UI events.
95
+ *
96
+ * Returns an empty array when the frame carries no user-visible
97
+ * information (internal bookkeeping events like ``harness.wake``).
98
+ * Returns multiple events when a single Surogates event corresponds
99
+ * to a chunk triad that the AG-UI client can't reconstruct on its
100
+ * own -- though we try to use chunk events to keep this to one
101
+ * output event per input in the common path.
102
+ */
103
+ translate(frame) {
104
+ switch (frame.type) {
105
+ case SURG_EVENT.LLM_DELTA:
106
+ return this.handleLlmDelta(frame);
107
+ case SURG_EVENT.LLM_RESPONSE:
108
+ return this.handleLlmResponse(frame);
109
+ case SURG_EVENT.LLM_THINKING:
110
+ return this.handleLlmThinking(frame);
111
+ case SURG_EVENT.TOOL_CALL:
112
+ return this.handleToolCall(frame);
113
+ case SURG_EVENT.TOOL_RESULT:
114
+ return this.handleToolResult(frame);
115
+ case SURG_EVENT.POLICY_DENIED:
116
+ return this.handlePolicyDenied(frame);
117
+ case SURG_EVENT.EXPERT_DELEGATION:
118
+ return this.handleExpertDelegation(frame);
119
+ case SURG_EVENT.EXPERT_RESULT:
120
+ return this.handleExpertResult(frame);
121
+ case SURG_EVENT.EXPERT_FAILURE:
122
+ return this.asCustom(frame);
123
+ case SURG_EVENT.SESSION_FAIL:
124
+ case SURG_EVENT.HARNESS_CRASH:
125
+ return this.handleTerminalFailure(frame);
126
+ case SURG_EVENT.SESSION_DONE:
127
+ case SURG_EVENT.SESSION_COMPLETE:
128
+ this._turnComplete = true;
129
+ return [];
130
+ // Frames we do not surface to AG-UI consumers: internal
131
+ // orchestration (``user.message`` is already in the consumer's
132
+ // own ``agent.messages``; ``llm.request``, ``session.start``,
133
+ // ``sandbox.*``, ``harness.wake`` are implementation details).
134
+ case SURG_EVENT.USER_MESSAGE:
135
+ case SURG_EVENT.LLM_REQUEST:
136
+ case SURG_EVENT.SESSION_START:
137
+ case SURG_EVENT.SESSION_PAUSE:
138
+ case SURG_EVENT.SESSION_RESUME:
139
+ case SURG_EVENT.SANDBOX_PROVISION:
140
+ case SURG_EVENT.SANDBOX_EXECUTE:
141
+ case SURG_EVENT.SANDBOX_RESULT:
142
+ case SURG_EVENT.SANDBOX_DESTROY:
143
+ case SURG_EVENT.POLICY_ALLOWED:
144
+ return [];
145
+ // Everything else (``memory.update``, ``context.compact``,
146
+ // saga events, future server additions) is forwarded as CUSTOM
147
+ // so advanced consumers that want that visibility can still
148
+ // subscribe -- without us pre-committing to a particular AG-UI
149
+ // shape for a Surogates-specific event that might evolve.
150
+ default:
151
+ return this.asCustom(frame);
152
+ }
153
+ }
154
+ // ------------------------------------------------------------------
155
+ // Individual handlers
156
+ // ------------------------------------------------------------------
157
+ handleLlmDelta(frame) {
158
+ const payload = frame.data ?? {};
159
+ const delta = payload.delta ?? payload.content ?? "";
160
+ if (!delta) return [];
161
+ const messageId = this.currentMessageId ?? payload.message_id ?? this.mintMessageId(frame.id);
162
+ this.currentMessageId = messageId;
163
+ return [
164
+ {
165
+ type: EventType.TEXT_MESSAGE_CHUNK,
166
+ messageId,
167
+ role: "assistant",
168
+ delta
169
+ }
170
+ ];
171
+ }
172
+ handleLlmResponse(frame) {
173
+ const payload = frame.data ?? {};
174
+ const events = [];
175
+ if (!this.currentMessageId && payload.content) {
176
+ const messageId = payload.message_id ?? payload.id ?? this.mintMessageId(frame.id);
177
+ events.push({
178
+ type: EventType.TEXT_MESSAGE_CHUNK,
179
+ messageId,
180
+ role: "assistant",
181
+ delta: payload.content
182
+ });
183
+ }
184
+ this.currentMessageId = void 0;
185
+ const toolCalls = Array.isArray(payload.tool_calls) ? payload.tool_calls : [];
186
+ const finish = payload.finish_reason ?? payload.stop_reason ?? "";
187
+ if (CONTINUATION_FINISH_REASONS.has(finish)) {
188
+ return events;
189
+ }
190
+ const modelDone = toolCalls.length === 0 && (finish === "" || END_OF_TURN_FINISH_REASONS.has(finish));
191
+ if (modelDone && this.pendingToolCalls.size === 0) {
192
+ this._turnComplete = true;
193
+ }
194
+ return events;
195
+ }
196
+ handleLlmThinking(frame) {
197
+ const payload = frame.data ?? {};
198
+ const delta = payload.delta ?? payload.content ?? "";
199
+ if (!delta) return [];
200
+ const messageId = this.currentReasoningId ?? payload.message_id ?? this.mintMessageId(frame.id);
201
+ const isNew = this.currentReasoningId !== messageId;
202
+ this.currentReasoningId = messageId;
203
+ const events = [];
204
+ if (isNew) {
205
+ events.push({
206
+ type: EventType.REASONING_START,
207
+ messageId
208
+ });
209
+ events.push({
210
+ type: EventType.REASONING_MESSAGE_START,
211
+ messageId,
212
+ role: "reasoning"
213
+ });
214
+ }
215
+ events.push({
216
+ type: EventType.REASONING_MESSAGE_CONTENT,
217
+ messageId,
218
+ delta
219
+ });
220
+ return events;
221
+ }
222
+ handleToolCall(frame) {
223
+ const payload = frame.data ?? {};
224
+ const toolCallId = payload.tool_call_id ?? `tc-${frame.id}`;
225
+ const toolCallName = payload.name ?? "unknown_tool";
226
+ this.currentMessageId = void 0;
227
+ this.currentReasoningId = void 0;
228
+ this.pendingToolCalls.add(toolCallId);
229
+ const args = typeof payload.arguments === "string" ? payload.arguments : JSON.stringify(payload.arguments ?? {});
230
+ return [
231
+ {
232
+ type: EventType.TOOL_CALL_CHUNK,
233
+ toolCallId,
234
+ toolCallName,
235
+ delta: args
236
+ }
237
+ ];
238
+ }
239
+ handleToolResult(frame) {
240
+ const payload = frame.data ?? {};
241
+ const toolCallId = payload.tool_call_id ?? `tc-${frame.id}`;
242
+ const content = payload.content ?? "";
243
+ this.pendingToolCalls.delete(toolCallId);
244
+ return [
245
+ {
246
+ type: EventType.TOOL_CALL_RESULT,
247
+ messageId: `tr-${frame.id}`,
248
+ toolCallId,
249
+ content,
250
+ role: "tool"
251
+ }
252
+ ];
253
+ }
254
+ handlePolicyDenied(frame) {
255
+ const payload = frame.data ?? {};
256
+ return [
257
+ {
258
+ type: EventType.CUSTOM,
259
+ name: SURG_EVENT.POLICY_DENIED,
260
+ value: { tool: payload.tool, reason: payload.reason }
261
+ }
262
+ ];
263
+ }
264
+ handleExpertDelegation(frame) {
265
+ const payload = frame.data ?? {};
266
+ const name = payload.expert_name ?? "expert";
267
+ return [
268
+ {
269
+ type: EventType.STEP_STARTED,
270
+ stepName: `expert:${name}`
271
+ }
272
+ ];
273
+ }
274
+ handleExpertResult(frame) {
275
+ const payload = frame.data ?? {};
276
+ const name = payload.expert_name ?? "expert";
277
+ return [
278
+ {
279
+ type: EventType.STEP_FINISHED,
280
+ stepName: `expert:${name}`
281
+ }
282
+ ];
283
+ }
284
+ handleTerminalFailure(frame) {
285
+ this._turnComplete = true;
286
+ const payload = frame.data ?? {};
287
+ const message = payload.error ?? payload.message ?? `${frame.type} fired`;
288
+ return [
289
+ {
290
+ type: EventType.RUN_ERROR,
291
+ message,
292
+ code: frame.type
293
+ }
294
+ ];
295
+ }
296
+ asCustom(frame) {
297
+ return [
298
+ {
299
+ type: EventType.CUSTOM,
300
+ name: frame.type,
301
+ value: frame.data
302
+ }
303
+ ];
304
+ }
305
+ /**
306
+ * Generate a stable message id derived from the triggering frame's
307
+ * event id. The event id is globally unique per session, so using
308
+ * it as a suffix guarantees uniqueness without pulling in ``uuid``.
309
+ */
310
+ mintMessageId(frameId) {
311
+ return `msg-${frameId}`;
312
+ }
313
+ };
314
+
315
+ // src/protocol.ts
316
+ function withAgentId(path, agentId) {
317
+ if (!agentId) return path;
318
+ const sep = path.includes("?") ? "&" : "?";
319
+ return `${path}${sep}agent_id=${encodeURIComponent(agentId)}`;
320
+ }
321
+ async function doFetch(url, init) {
322
+ let response;
323
+ try {
324
+ response = await fetch(url, {
325
+ ...init,
326
+ credentials: "include",
327
+ headers: {
328
+ ...init.headers,
329
+ [VERSION_HEADER]: SDK_VERSION
330
+ }
331
+ });
332
+ } catch (cause) {
333
+ throw new SurogatesNetworkError("Network request failed", { cause });
334
+ }
335
+ return response;
336
+ }
337
+ async function extractDetail(response) {
338
+ try {
339
+ const body = await response.json();
340
+ return typeof body.detail === "string" ? body.detail : void 0;
341
+ } catch {
342
+ return void 0;
343
+ }
344
+ }
345
+ async function raiseForStatus(response, action) {
346
+ const detail = await extractDetail(response);
347
+ if (response.status === 401 || response.status === 403) {
348
+ throw new SurogatesAuthError(`${action} rejected (${response.status})`, {
349
+ status: response.status,
350
+ ...detail !== void 0 && { detail }
351
+ });
352
+ }
353
+ if (response.status === 429) {
354
+ const retryAfterHeader = response.headers.get("Retry-After");
355
+ const retryAfter = retryAfterHeader ? Number(retryAfterHeader) : void 0;
356
+ throw new SurogatesRateLimitError(`${action} rate-limited`, {
357
+ status: response.status,
358
+ ...retryAfter !== void 0 && Number.isFinite(retryAfter) && { retryAfter },
359
+ ...detail !== void 0 && { detail }
360
+ });
361
+ }
362
+ if (response.status >= 500) {
363
+ throw new SurogatesNetworkError(
364
+ `${action} failed (${response.status}${detail ? `: ${detail}` : ""})`
365
+ );
366
+ }
367
+ throw new SurogatesProtocolError(`${action} failed (${response.status})`, {
368
+ status: response.status,
369
+ ...detail !== void 0 && { detail }
370
+ });
371
+ }
372
+ async function bootstrap(apiUrl, publishableKey, agentId) {
373
+ const response = await doFetch(apiUrl + withAgentId(PATH_BOOTSTRAP, agentId), {
374
+ method: "POST",
375
+ headers: {
376
+ Authorization: `Bearer ${publishableKey}`,
377
+ "Content-Type": "application/json"
378
+ }
379
+ });
380
+ if (!response.ok) {
381
+ await raiseForStatus(response, "Bootstrap");
382
+ }
383
+ let body;
384
+ try {
385
+ body = await response.json();
386
+ } catch (cause) {
387
+ throw new SurogatesProtocolError("Bootstrap returned non-JSON body", { cause });
388
+ }
389
+ if (!body || typeof body.session_id !== "string" || typeof body.csrf_token !== "string") {
390
+ throw new SurogatesProtocolError("Bootstrap response missing required fields");
391
+ }
392
+ return {
393
+ sessionId: body.session_id,
394
+ csrfToken: body.csrf_token,
395
+ expiresAt: Number(body.expires_at) || 0,
396
+ agentName: body.agent_name ?? ""
397
+ };
398
+ }
399
+ async function sendMessage(apiUrl, sessionId, csrfToken, content, agentId) {
400
+ const response = await doFetch(apiUrl + withAgentId(PATH_MESSAGES(sessionId), agentId), {
401
+ method: "POST",
402
+ headers: {
403
+ "Content-Type": "application/json",
404
+ [CSRF_HEADER]: csrfToken
405
+ },
406
+ body: JSON.stringify({ content })
407
+ });
408
+ if (!response.ok) {
409
+ await raiseForStatus(response, "Send message");
410
+ }
411
+ const body = await response.json().catch(() => ({}));
412
+ return { eventId: Number(body.event_id ?? 0) };
413
+ }
414
+ async function endSession(apiUrl, sessionId, csrfToken, agentId) {
415
+ try {
416
+ await doFetch(apiUrl + withAgentId(PATH_END(sessionId), agentId), {
417
+ method: "POST",
418
+ headers: { [CSRF_HEADER]: csrfToken }
419
+ });
420
+ } catch {
421
+ }
422
+ }
423
+ function openEventStream(apiUrl, sessionId, cursor, agentId) {
424
+ const url = apiUrl + withAgentId(PATH_EVENTS(sessionId, cursor), agentId);
425
+ return new EventSource(url, { withCredentials: true });
426
+ }
427
+
428
+ // src/agent.ts
429
+ var SURG_EVENT_NAMES = Object.values(SURG_EVENT);
430
+ var DRAIN_WINDOW_MS = 250;
431
+ var WebsiteAgent = class extends AbstractAgent {
432
+ constructor(config) {
433
+ super(config);
434
+ /** Monotonic cursor across runs. Passed to the SSE ``?after=`` param. */
435
+ this.cursor = 0;
436
+ if (!config.apiUrl) {
437
+ throw new AGUIError('WebsiteAgent: "apiUrl" is required.');
438
+ }
439
+ if (!config.publishableKey) {
440
+ throw new AGUIError('WebsiteAgent: "publishableKey" is required.');
441
+ }
442
+ this.apiUrl = config.apiUrl.replace(/\/+$/, "");
443
+ this.publishableKey = config.publishableKey;
444
+ }
445
+ /**
446
+ * Ensure we have a live session cookie + CSRF token.
447
+ *
448
+ * Idempotent; returns the cached result when one already exists.
449
+ * ``bootstrap()`` is the only call that requires the publishable
450
+ * key to travel in an Authorization header, so callers that want
451
+ * to eagerly validate their configuration (e.g. to surface a
452
+ * setup error at widget-load time) can invoke this directly.
453
+ */
454
+ async ensureBootstrapped() {
455
+ if (this.bootstrapResult) return this.bootstrapResult;
456
+ this.bootstrapResult = await bootstrap(
457
+ this.apiUrl,
458
+ this.publishableKey,
459
+ this.agentId
460
+ );
461
+ return this.bootstrapResult;
462
+ }
463
+ /**
464
+ * Close the current session on the server side and drop local state.
465
+ *
466
+ * Callers should invoke this when the visitor closes the chat UI
467
+ * so the server can mark the session complete instead of waiting
468
+ * for the idle-reset timer. Safe to call when no session has been
469
+ * bootstrapped yet.
470
+ */
471
+ async end() {
472
+ const current = this.bootstrapResult;
473
+ if (!current) return;
474
+ this.bootstrapResult = void 0;
475
+ this.cursor = 0;
476
+ await endSession(this.apiUrl, current.sessionId, current.csrfToken, this.agentId);
477
+ }
478
+ /**
479
+ * Implementation of the AG-UI ``run()`` contract.
480
+ *
481
+ * Returns a cold observable: one subscription per run. AG-UI's
482
+ * ``runAgent`` pipeline pipes this through chunk transformation,
483
+ * verification, and subscriber notifications before updating
484
+ * ``this.messages``.
485
+ */
486
+ run(input) {
487
+ return new Observable((subscriber) => {
488
+ const ctx = {
489
+ terminated: false,
490
+ source: void 0,
491
+ drainTimer: void 0
492
+ };
493
+ void this.driveRun(input, subscriber, ctx).catch((err) => {
494
+ if (!ctx.terminated) {
495
+ ctx.terminated = true;
496
+ subscriber.error(err);
497
+ }
498
+ });
499
+ return () => {
500
+ ctx.terminated = true;
501
+ if (ctx.drainTimer !== void 0) return;
502
+ closeEventSource(ctx.source);
503
+ ctx.source = void 0;
504
+ };
505
+ });
506
+ }
507
+ /**
508
+ * Async body of the observable factory.
509
+ *
510
+ * Emits ``RUN_STARTED`` up front so every downstream failure is
511
+ * well-bracketed by the AG-UI lifecycle. ``RUN_FINISHED`` is
512
+ * emitted by the SSE listeners when the translator detects end-of-
513
+ * turn; ``RUN_ERROR`` is emitted from the catch below when bootstrap
514
+ * or message-send fails before the SSE has any chance to close out.
515
+ *
516
+ * The ``ctx.terminated`` guard is the single enforcement point for
517
+ * AG-UI's "exactly one terminal event" invariant. Every emission
518
+ * site checks it before calling ``subscriber.next`` with RUN_FINISHED
519
+ * or RUN_ERROR, and every terminal call sets it to true first.
520
+ */
521
+ async driveRun(input, subscriber, ctx) {
522
+ if (ctx.terminated) return;
523
+ subscriber.next({
524
+ type: EventType.RUN_STARTED,
525
+ threadId: input.threadId,
526
+ runId: input.runId
527
+ });
528
+ try {
529
+ const userText = this.extractUserText(input.messages);
530
+ const boot = await this.ensureBootstrapped();
531
+ this.replaceStreamSource(
532
+ ctx,
533
+ boot.sessionId,
534
+ this.cursor,
535
+ new Translator(),
536
+ subscriber,
537
+ input
538
+ );
539
+ await this.sendWithCookieRecovery(boot, userText, (fresh) => {
540
+ this.replaceStreamSource(
541
+ ctx,
542
+ fresh.sessionId,
543
+ 0,
544
+ new Translator(),
545
+ subscriber,
546
+ input
547
+ );
548
+ });
549
+ } catch (err) {
550
+ if (ctx.terminated) return;
551
+ ctx.terminated = true;
552
+ const message = err instanceof Error ? err.message : String(err);
553
+ const code = err instanceof SurogatesAuthError ? "auth" : "error";
554
+ subscriber.next({
555
+ type: EventType.RUN_ERROR,
556
+ message,
557
+ code
558
+ });
559
+ closeEventSource(ctx.source);
560
+ ctx.source = void 0;
561
+ subscriber.complete();
562
+ }
563
+ }
564
+ /**
565
+ * Tear down whatever EventSource is currently live on ``ctx`` and
566
+ * open a fresh one against *sessionId* starting at *after*. Called
567
+ * both for the initial stream and after cookie recovery. Always
568
+ * clears the stale ``onerror`` first so the old source can't race
569
+ * a ``RUN_ERROR`` into the subscriber after it's been replaced.
570
+ */
571
+ replaceStreamSource(ctx, sessionId, after, translator, subscriber, input) {
572
+ closeEventSource(ctx.source);
573
+ const source = openEventStream(this.apiUrl, sessionId, after, this.agentId);
574
+ ctx.source = source;
575
+ this.attachStreamListeners(source, translator, subscriber, input, ctx);
576
+ }
577
+ /**
578
+ * Try ``sendMessage``; on auth failure re-bootstrap once and retry.
579
+ *
580
+ * The re-bootstrap mints a new session id, so the caller passes an
581
+ * ``onRebootstrap`` callback that reopens the SSE stream against the
582
+ * fresh session before the retry. Visitor continuity across
583
+ * bootstraps is a deliberate non-feature for v1; the new session
584
+ * starts empty. Only frames emitted on the OLD session between
585
+ * SSE-open and send-failure are lost -- and those are always
586
+ * previous-turn events, since the current turn's ``sendMessage`` is
587
+ * what 401-ed.
588
+ */
589
+ async sendWithCookieRecovery(boot, content, onRebootstrap) {
590
+ try {
591
+ await sendMessage(this.apiUrl, boot.sessionId, boot.csrfToken, content, this.agentId);
592
+ return;
593
+ } catch (err) {
594
+ if (!(err instanceof SurogatesAuthError)) throw err;
595
+ this.bootstrapResult = void 0;
596
+ this.cursor = 0;
597
+ const fresh = await this.ensureBootstrapped();
598
+ onRebootstrap(fresh);
599
+ await sendMessage(this.apiUrl, fresh.sessionId, fresh.csrfToken, content, this.agentId);
600
+ }
601
+ }
602
+ /**
603
+ * Wire up the EventSource to translate and forward every frame,
604
+ * and terminate the observable when the translator flags
605
+ * end-of-turn or a transport failure occurs.
606
+ *
607
+ * After the translator flips end-of-turn we emit ``RUN_FINISHED``
608
+ * immediately, then keep the stream open for ``DRAIN_WINDOW_MS``
609
+ * so trailing frames (late ``memory.update``, the sentinel
610
+ * ``session.done``) advance ``this.cursor`` rather than arriving
611
+ * on the next run. The ``ctx.terminated`` gate stops additional
612
+ * subscriber emissions during the drain.
613
+ */
614
+ attachStreamListeners(source, translator, subscriber, input, ctx) {
615
+ const scheduleDrainClose = () => {
616
+ if (ctx.drainTimer !== void 0) globalThis.clearTimeout(ctx.drainTimer);
617
+ ctx.drainTimer = globalThis.setTimeout(() => {
618
+ ctx.drainTimer = void 0;
619
+ closeEventSource(source);
620
+ }, DRAIN_WINDOW_MS);
621
+ };
622
+ const finish = () => {
623
+ if (ctx.terminated) return;
624
+ ctx.terminated = true;
625
+ subscriber.next({
626
+ type: EventType.RUN_FINISHED,
627
+ threadId: input.threadId,
628
+ runId: input.runId
629
+ });
630
+ scheduleDrainClose();
631
+ subscriber.complete();
632
+ };
633
+ const handleFrame = (name, data, lastEventId) => {
634
+ let parsed;
635
+ try {
636
+ parsed = JSON.parse(data);
637
+ } catch {
638
+ parsed = data;
639
+ }
640
+ const frame = {
641
+ id: Number(lastEventId) || 0,
642
+ type: name,
643
+ data: parsed
644
+ };
645
+ if (frame.id > this.cursor) this.cursor = frame.id;
646
+ if (ctx.terminated) return;
647
+ const events = translator.translate(frame);
648
+ for (const e of events) subscriber.next(e);
649
+ if (translator.isTurnComplete) finish();
650
+ };
651
+ for (const name of SURG_EVENT_NAMES) {
652
+ source.addEventListener(name, (ev) => {
653
+ handleFrame(name, ev.data, ev.lastEventId);
654
+ });
655
+ }
656
+ source.onerror = () => {
657
+ if (source.readyState !== 2) return;
658
+ if (ctx.terminated) return;
659
+ ctx.terminated = true;
660
+ subscriber.next({
661
+ type: EventType.RUN_ERROR,
662
+ message: "SSE connection closed",
663
+ code: "sse_closed"
664
+ });
665
+ subscriber.complete();
666
+ };
667
+ }
668
+ /**
669
+ * Find the last user-authored message body to post. AG-UI supports
670
+ * multimodal content arrays; this v1 only handles string content
671
+ * (the website channel's ``POST /messages`` endpoint accepts plain
672
+ * text). Multimodal support lands once the server grows a media
673
+ * upload path.
674
+ */
675
+ extractUserText(messages) {
676
+ for (let i = messages.length - 1; i >= 0; i--) {
677
+ const m = messages[i];
678
+ if (!m || m.role !== "user") continue;
679
+ const content = m.content;
680
+ if (typeof content === "string" && content.trim().length > 0) {
681
+ return content;
682
+ }
683
+ }
684
+ throw new AGUIError(
685
+ "WebsiteAgent: no user message with string content found in input.messages."
686
+ );
687
+ }
688
+ };
689
+ function closeEventSource(source) {
690
+ if (!source) return;
691
+ try {
692
+ source.onerror = null;
693
+ } catch {
694
+ }
695
+ source.close();
696
+ }
697
+
698
+ export { PROTOCOL_VERSION, SDK_VERSION, SURG_EVENT, SurogatesAuthError, SurogatesError, SurogatesNetworkError, SurogatesProtocolError, SurogatesRateLimitError, Translator, WebsiteAgent };
699
+ //# sourceMappingURL=chunk-MAUJDM7P.js.map
700
+ //# sourceMappingURL=chunk-MAUJDM7P.js.map