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