@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/LICENSE.AGPL-3.0 +661 -0
- package/README.md +175 -0
- package/dist/index.cjs +712 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +406 -0
- package/dist/index.d.ts +406 -0
- package/dist/index.js +694 -0
- package/dist/index.js.map +1 -0
- package/dist/surogates-widget.global.js +26 -0
- package/dist/surogates-widget.global.js.map +1 -0
- package/package.json +69 -0
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
|