@band-ai/sdk 0.1.2
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/CodexAdapter-A1k_LMdZ.d.ts +1278 -0
- package/dist/CodexAdapter-BbZD-nBd.d.cts +1278 -0
- package/dist/adapters.cjs +10643 -0
- package/dist/adapters.d.cts +217 -0
- package/dist/adapters.d.ts +217 -0
- package/dist/adapters.js +92 -0
- package/dist/backends-BnOLsG7w.d.cts +20 -0
- package/dist/backends-C4n6cKOf.d.ts +20 -0
- package/dist/chunk-3BKAC4OZ.js +44 -0
- package/dist/chunk-5CXIMAH2.js +52 -0
- package/dist/chunk-6AJA2OPC.js +3682 -0
- package/dist/chunk-A3TDK6GP.js +6674 -0
- package/dist/chunk-AVVDPXP4.js +39 -0
- package/dist/chunk-EDPKUUJL.js +177 -0
- package/dist/chunk-FUODYQRE.js +2562 -0
- package/dist/chunk-GVEQ3RAH.js +373 -0
- package/dist/chunk-INA6YZTZ.js +88 -0
- package/dist/chunk-IZPV6QS6.js +115 -0
- package/dist/chunk-LY55KYVI.js +0 -0
- package/dist/chunk-NIAQBANR.js +577 -0
- package/dist/chunk-OD2G5LFQ.js +78 -0
- package/dist/chunk-SFVKOEQH.js +103 -0
- package/dist/chunk-SG4WLD2H.js +1093 -0
- package/dist/chunk-YR3BOQNW.js +39 -0
- package/dist/chunk-ZZJRRBPQ.js +82 -0
- package/dist/claude-CcnQ5OUC.d.ts +53 -0
- package/dist/claude-CwTAxhwI.d.cts +53 -0
- package/dist/config.cjs +164 -0
- package/dist/config.d.cts +19 -0
- package/dist/config.d.ts +19 -0
- package/dist/config.js +9 -0
- package/dist/converters.cjs +1086 -0
- package/dist/converters.d.cts +143 -0
- package/dist/converters.d.ts +143 -0
- package/dist/converters.js +468 -0
- package/dist/core.cjs +185 -0
- package/dist/core.d.cts +22 -0
- package/dist/core.d.ts +22 -0
- package/dist/core.js +24 -0
- package/dist/customTools-BzF0IISO.d.cts +10 -0
- package/dist/customTools-BzF0IISO.d.ts +10 -0
- package/dist/dtos-DUpbIu8k.d.cts +163 -0
- package/dist/dtos-DUpbIu8k.d.ts +163 -0
- package/dist/history-ByessXNq.d.ts +24 -0
- package/dist/history-i6yN7neC.d.cts +24 -0
- package/dist/index.cjs +13914 -0
- package/dist/index.d.cts +63 -0
- package/dist/index.d.ts +63 -0
- package/dist/index.js +198 -0
- package/dist/linear.cjs +2207 -0
- package/dist/linear.d.cts +167 -0
- package/dist/linear.d.ts +167 -0
- package/dist/linear.js +2139 -0
- package/dist/logger-CABQOnlR.d.cts +21 -0
- package/dist/logger-CABQOnlR.d.ts +21 -0
- package/dist/mcp-claude.cjs +584 -0
- package/dist/mcp-claude.d.cts +4 -0
- package/dist/mcp-claude.d.ts +4 -0
- package/dist/mcp-claude.js +11 -0
- package/dist/mcp.cjs +1247 -0
- package/dist/mcp.d.cts +97 -0
- package/dist/mcp.d.ts +97 -0
- package/dist/mcp.js +27 -0
- package/dist/opencode-BBcDchcN.d.cts +159 -0
- package/dist/opencode-DthQQUsZ.d.ts +159 -0
- package/dist/pagination-BPiys10t.d.cts +25 -0
- package/dist/pagination-CcDkVFxK.d.ts +25 -0
- package/dist/protocols-CInhtFpA.d.cts +116 -0
- package/dist/protocols-DrhzOH6K.d.ts +116 -0
- package/dist/rest.cjs +1159 -0
- package/dist/rest.d.cts +144 -0
- package/dist/rest.d.ts +144 -0
- package/dist/rest.js +18 -0
- package/dist/runtime.cjs +5353 -0
- package/dist/runtime.d.cts +276 -0
- package/dist/runtime.d.ts +276 -0
- package/dist/runtime.js +274 -0
- package/dist/schemas-BpEBkq5V.d.cts +592 -0
- package/dist/schemas-vos1AaBc.d.ts +592 -0
- package/dist/sdk-HJUVSSWA.js +10 -0
- package/dist/simpleAdapter-9OE_p6QW.d.ts +29 -0
- package/dist/simpleAdapter-DqhvjsGr.d.cts +29 -0
- package/dist/testing.cjs +217 -0
- package/dist/testing.d.cts +126 -0
- package/dist/testing.d.ts +126 -0
- package/dist/testing.js +186 -0
- package/dist/types-7jVzOeOP.d.ts +446 -0
- package/dist/types-CSHFipIc.d.cts +331 -0
- package/dist/types-CovcHpYf.d.cts +446 -0
- package/dist/types-CxII3XkA.d.ts +331 -0
- package/package.json +189 -0
|
@@ -0,0 +1,2562 @@
|
|
|
1
|
+
import {
|
|
2
|
+
SimpleAdapter
|
|
3
|
+
} from "./chunk-AVVDPXP4.js";
|
|
4
|
+
import {
|
|
5
|
+
renderSystemPrompt
|
|
6
|
+
} from "./chunk-SFVKOEQH.js";
|
|
7
|
+
import {
|
|
8
|
+
asErrorMessage,
|
|
9
|
+
asNonEmptyString,
|
|
10
|
+
asOptionalRecord
|
|
11
|
+
} from "./chunk-ZZJRRBPQ.js";
|
|
12
|
+
import {
|
|
13
|
+
NoopLogger
|
|
14
|
+
} from "./chunk-OD2G5LFQ.js";
|
|
15
|
+
import {
|
|
16
|
+
UnsupportedFeatureError,
|
|
17
|
+
ValidationError
|
|
18
|
+
} from "./chunk-YR3BOQNW.js";
|
|
19
|
+
|
|
20
|
+
// src/converters/acp-client.ts
|
|
21
|
+
var ACPClientHistoryConverter = class {
|
|
22
|
+
convert(raw) {
|
|
23
|
+
const roomToSession = {};
|
|
24
|
+
for (const entry of raw) {
|
|
25
|
+
const metadataRaw = entry.metadata;
|
|
26
|
+
if (!metadataRaw || typeof metadataRaw !== "object" || Array.isArray(metadataRaw)) {
|
|
27
|
+
continue;
|
|
28
|
+
}
|
|
29
|
+
const metadata = metadataRaw;
|
|
30
|
+
const sessionId = metadata.acp_client_session_id;
|
|
31
|
+
const roomId = metadata.acp_client_room_id;
|
|
32
|
+
if (typeof sessionId === "string" && typeof roomId === "string" && sessionId && roomId) {
|
|
33
|
+
roomToSession[roomId] = sessionId;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
return { roomToSession };
|
|
37
|
+
}
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
// src/converters/acp-server.ts
|
|
41
|
+
var ACPServerHistoryConverter = class {
|
|
42
|
+
convert(raw) {
|
|
43
|
+
const sessionToRoom = {};
|
|
44
|
+
const sessionCwd = {};
|
|
45
|
+
const sessionMcpServers = {};
|
|
46
|
+
for (const entry of raw) {
|
|
47
|
+
const metadataRaw = entry.metadata;
|
|
48
|
+
if (!metadataRaw || typeof metadataRaw !== "object" || Array.isArray(metadataRaw)) {
|
|
49
|
+
continue;
|
|
50
|
+
}
|
|
51
|
+
const metadata = metadataRaw;
|
|
52
|
+
const sessionId = metadata.acp_session_id;
|
|
53
|
+
if (typeof sessionId !== "string" || sessionId.length === 0) {
|
|
54
|
+
continue;
|
|
55
|
+
}
|
|
56
|
+
const roomId = typeof metadata.acp_room_id === "string" ? metadata.acp_room_id : typeof entry.room_id === "string" ? entry.room_id : null;
|
|
57
|
+
if (roomId) {
|
|
58
|
+
sessionToRoom[sessionId] = roomId;
|
|
59
|
+
}
|
|
60
|
+
if (typeof metadata.acp_cwd === "string" && metadata.acp_cwd.length > 0) {
|
|
61
|
+
sessionCwd[sessionId] = metadata.acp_cwd;
|
|
62
|
+
}
|
|
63
|
+
if (Array.isArray(metadata.acp_mcp_servers)) {
|
|
64
|
+
sessionMcpServers[sessionId] = metadata.acp_mcp_servers.filter((server) => !!server && typeof server === "object" && !Array.isArray(server));
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
return {
|
|
68
|
+
sessionToRoom,
|
|
69
|
+
sessionCwd,
|
|
70
|
+
sessionMcpServers
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
// src/adapters/shared/history.ts
|
|
76
|
+
function findLatestTaskMetadata(raw, predicate) {
|
|
77
|
+
for (let index = raw.length - 1; index >= 0; index -= 1) {
|
|
78
|
+
const message = raw[index] ?? {};
|
|
79
|
+
const messageType = String(message.message_type ?? message.messageType ?? "");
|
|
80
|
+
if (messageType !== "task") {
|
|
81
|
+
continue;
|
|
82
|
+
}
|
|
83
|
+
const metadata = asOptionalRecord(message.metadata) ?? {};
|
|
84
|
+
if (predicate(metadata)) {
|
|
85
|
+
return metadata;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
return null;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// src/converters/shared.ts
|
|
92
|
+
function asOptionalString(value) {
|
|
93
|
+
if (typeof value !== "string") {
|
|
94
|
+
return null;
|
|
95
|
+
}
|
|
96
|
+
const trimmed = value.trim();
|
|
97
|
+
return trimmed.length > 0 ? trimmed : null;
|
|
98
|
+
}
|
|
99
|
+
function parseDate(value) {
|
|
100
|
+
if (typeof value !== "string" || value.length === 0) {
|
|
101
|
+
return null;
|
|
102
|
+
}
|
|
103
|
+
const parsed = new Date(value);
|
|
104
|
+
return Number.isNaN(parsed.getTime()) ? null : parsed;
|
|
105
|
+
}
|
|
106
|
+
function parseToolPayload(value) {
|
|
107
|
+
if (typeof value !== "string" || value.length === 0) {
|
|
108
|
+
return null;
|
|
109
|
+
}
|
|
110
|
+
try {
|
|
111
|
+
const parsed = JSON.parse(value);
|
|
112
|
+
return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : null;
|
|
113
|
+
} catch {
|
|
114
|
+
return null;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
function parseToolCall(value) {
|
|
118
|
+
const parsed = parseToolPayload(value);
|
|
119
|
+
const name = asOptionalString(parsed?.name);
|
|
120
|
+
const toolCallId = asOptionalString(parsed?.tool_call_id);
|
|
121
|
+
if (!name || !toolCallId) {
|
|
122
|
+
return null;
|
|
123
|
+
}
|
|
124
|
+
const args = parsed?.args;
|
|
125
|
+
return {
|
|
126
|
+
name,
|
|
127
|
+
toolCallId,
|
|
128
|
+
args: args && typeof args === "object" && !Array.isArray(args) ? args : {}
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
function parseToolResult(value) {
|
|
132
|
+
const parsed = parseToolPayload(value);
|
|
133
|
+
const name = asOptionalString(parsed?.name);
|
|
134
|
+
const toolCallId = asOptionalString(parsed?.tool_call_id);
|
|
135
|
+
if (!name || !toolCallId) {
|
|
136
|
+
return null;
|
|
137
|
+
}
|
|
138
|
+
return {
|
|
139
|
+
name,
|
|
140
|
+
toolCallId,
|
|
141
|
+
output: parsed?.output === void 0 ? "" : String(parsed.output),
|
|
142
|
+
isError: parsed?.is_error === true
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// src/converters/google-adk.ts
|
|
147
|
+
function appendToolCalls(messages, pendingToolCalls) {
|
|
148
|
+
if (pendingToolCalls.length === 0) {
|
|
149
|
+
return;
|
|
150
|
+
}
|
|
151
|
+
messages.push({
|
|
152
|
+
role: "model",
|
|
153
|
+
content: [...pendingToolCalls]
|
|
154
|
+
});
|
|
155
|
+
pendingToolCalls.length = 0;
|
|
156
|
+
}
|
|
157
|
+
function appendToolResults(messages, pendingToolResults) {
|
|
158
|
+
if (pendingToolResults.length === 0) {
|
|
159
|
+
return;
|
|
160
|
+
}
|
|
161
|
+
messages.push({
|
|
162
|
+
role: "user",
|
|
163
|
+
content: [...pendingToolResults]
|
|
164
|
+
});
|
|
165
|
+
pendingToolResults.length = 0;
|
|
166
|
+
}
|
|
167
|
+
function patchOrphanedToolCalls(messages) {
|
|
168
|
+
for (let index = 0; index < messages.length; index += 1) {
|
|
169
|
+
const message = messages[index];
|
|
170
|
+
if (message.role !== "model" || !Array.isArray(message.content)) {
|
|
171
|
+
continue;
|
|
172
|
+
}
|
|
173
|
+
const callNames = /* @__PURE__ */ new Map();
|
|
174
|
+
for (const block of message.content) {
|
|
175
|
+
if (block.type === "function_call" && typeof block.id === "string" && block.id.length > 0) {
|
|
176
|
+
callNames.set(block.id, typeof block.name === "string" ? block.name : "");
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
if (callNames.size === 0) {
|
|
180
|
+
continue;
|
|
181
|
+
}
|
|
182
|
+
const nextMessage = messages[index + 1];
|
|
183
|
+
const matchedIds = /* @__PURE__ */ new Set();
|
|
184
|
+
if (nextMessage?.role === "user" && Array.isArray(nextMessage.content)) {
|
|
185
|
+
for (const block of nextMessage.content) {
|
|
186
|
+
if (block.type === "function_response" && typeof block.tool_call_id === "string" && callNames.has(block.tool_call_id)) {
|
|
187
|
+
matchedIds.add(block.tool_call_id);
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
const orphanedIds = [...callNames.keys()].filter((toolCallId) => !matchedIds.has(toolCallId));
|
|
192
|
+
if (orphanedIds.length === 0) {
|
|
193
|
+
continue;
|
|
194
|
+
}
|
|
195
|
+
const syntheticResponses = orphanedIds.map((toolCallId) => ({
|
|
196
|
+
type: "function_response",
|
|
197
|
+
tool_call_id: toolCallId,
|
|
198
|
+
name: callNames.get(toolCallId) ?? "",
|
|
199
|
+
output: "Error: tool execution was interrupted",
|
|
200
|
+
is_error: true
|
|
201
|
+
}));
|
|
202
|
+
if (nextMessage?.role === "user") {
|
|
203
|
+
if (typeof nextMessage.content === "string") {
|
|
204
|
+
nextMessage.content = [
|
|
205
|
+
...syntheticResponses,
|
|
206
|
+
{ type: "text", text: nextMessage.content }
|
|
207
|
+
];
|
|
208
|
+
} else if (Array.isArray(nextMessage.content)) {
|
|
209
|
+
nextMessage.content = [...syntheticResponses, ...nextMessage.content];
|
|
210
|
+
}
|
|
211
|
+
continue;
|
|
212
|
+
}
|
|
213
|
+
messages.splice(index + 1, 0, {
|
|
214
|
+
role: "user",
|
|
215
|
+
content: syntheticResponses
|
|
216
|
+
});
|
|
217
|
+
index += 1;
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
var GoogleADKHistoryConverter = class {
|
|
221
|
+
agentName = "";
|
|
222
|
+
constructor(agentName = "") {
|
|
223
|
+
this.agentName = agentName;
|
|
224
|
+
}
|
|
225
|
+
setAgentName(name) {
|
|
226
|
+
this.agentName = name;
|
|
227
|
+
}
|
|
228
|
+
convert(raw) {
|
|
229
|
+
const messages = [];
|
|
230
|
+
const pendingToolCalls = [];
|
|
231
|
+
const pendingToolResults = [];
|
|
232
|
+
for (const entry of raw) {
|
|
233
|
+
const messageType = String(entry.message_type ?? "text");
|
|
234
|
+
const content = String(entry.content ?? "");
|
|
235
|
+
if (messageType === "tool_call") {
|
|
236
|
+
appendToolResults(messages, pendingToolResults);
|
|
237
|
+
const parsed = parseToolCall(content);
|
|
238
|
+
if (parsed) {
|
|
239
|
+
pendingToolCalls.push({
|
|
240
|
+
type: "function_call",
|
|
241
|
+
id: parsed.toolCallId,
|
|
242
|
+
name: parsed.name,
|
|
243
|
+
args: parsed.args
|
|
244
|
+
});
|
|
245
|
+
}
|
|
246
|
+
continue;
|
|
247
|
+
}
|
|
248
|
+
if (messageType === "tool_result") {
|
|
249
|
+
appendToolCalls(messages, pendingToolCalls);
|
|
250
|
+
const parsed = parseToolResult(content);
|
|
251
|
+
if (parsed) {
|
|
252
|
+
pendingToolResults.push({
|
|
253
|
+
type: "function_response",
|
|
254
|
+
tool_call_id: parsed.toolCallId,
|
|
255
|
+
name: parsed.name,
|
|
256
|
+
output: parsed.output,
|
|
257
|
+
...parsed.isError ? { is_error: true } : {}
|
|
258
|
+
});
|
|
259
|
+
}
|
|
260
|
+
continue;
|
|
261
|
+
}
|
|
262
|
+
if (messageType === "thought" || messageType === "error") {
|
|
263
|
+
continue;
|
|
264
|
+
}
|
|
265
|
+
appendToolCalls(messages, pendingToolCalls);
|
|
266
|
+
appendToolResults(messages, pendingToolResults);
|
|
267
|
+
const senderName = String(entry.sender_name ?? "");
|
|
268
|
+
const role = String(entry.role ?? "user");
|
|
269
|
+
if (role === "assistant" && senderName === this.agentName) {
|
|
270
|
+
continue;
|
|
271
|
+
}
|
|
272
|
+
messages.push({
|
|
273
|
+
role: "user",
|
|
274
|
+
content: senderName ? `[${senderName}]: ${content}` : content
|
|
275
|
+
});
|
|
276
|
+
}
|
|
277
|
+
appendToolCalls(messages, pendingToolCalls);
|
|
278
|
+
appendToolResults(messages, pendingToolResults);
|
|
279
|
+
patchOrphanedToolCalls(messages);
|
|
280
|
+
return messages;
|
|
281
|
+
}
|
|
282
|
+
};
|
|
283
|
+
function extractGoogleAdkSessionId(raw) {
|
|
284
|
+
const metadata = findLatestTaskMetadata(
|
|
285
|
+
raw,
|
|
286
|
+
(entry) => typeof entry.google_adk_session_id === "string" && entry.google_adk_session_id.length > 0
|
|
287
|
+
);
|
|
288
|
+
const sessionId = metadata?.google_adk_session_id;
|
|
289
|
+
return typeof sessionId === "string" && sessionId.length > 0 ? sessionId : null;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
// src/adapters/a2a/types.ts
|
|
293
|
+
function buildA2AAuthHeaders(auth) {
|
|
294
|
+
const headers = {};
|
|
295
|
+
for (const [key, value] of Object.entries(auth?.headers ?? {})) {
|
|
296
|
+
headers[key] = sanitizeHeaderValue(value, key);
|
|
297
|
+
}
|
|
298
|
+
if (auth?.apiKey) {
|
|
299
|
+
headers["X-API-Key"] = sanitizeHeaderValue(auth.apiKey, "X-API-Key");
|
|
300
|
+
}
|
|
301
|
+
if (auth?.bearerToken) {
|
|
302
|
+
headers.Authorization = `Bearer ${sanitizeHeaderValue(auth.bearerToken, "Authorization")}`;
|
|
303
|
+
}
|
|
304
|
+
return headers;
|
|
305
|
+
}
|
|
306
|
+
function sanitizeHeaderValue(value, headerName) {
|
|
307
|
+
if (/[\r\n]/u.test(value)) {
|
|
308
|
+
throw new Error(`${headerName} header value must not contain CR or LF characters.`);
|
|
309
|
+
}
|
|
310
|
+
return value;
|
|
311
|
+
}
|
|
312
|
+
var A2AHistoryConverter = class {
|
|
313
|
+
convert(raw) {
|
|
314
|
+
const metadata = findLatestTaskMetadata(
|
|
315
|
+
raw,
|
|
316
|
+
(entry) => Object.prototype.hasOwnProperty.call(entry, "a2a_context_id")
|
|
317
|
+
);
|
|
318
|
+
if (metadata) {
|
|
319
|
+
return {
|
|
320
|
+
contextId: asNonEmptyString(metadata.a2a_context_id),
|
|
321
|
+
taskId: asNonEmptyString(metadata.a2a_task_id),
|
|
322
|
+
taskState: asNonEmptyString(metadata.a2a_task_state)
|
|
323
|
+
};
|
|
324
|
+
}
|
|
325
|
+
return {
|
|
326
|
+
contextId: null,
|
|
327
|
+
taskId: null,
|
|
328
|
+
taskState: null
|
|
329
|
+
};
|
|
330
|
+
}
|
|
331
|
+
};
|
|
332
|
+
|
|
333
|
+
// src/adapters/a2a/A2AAdapter.ts
|
|
334
|
+
import { randomUUID } from "crypto";
|
|
335
|
+
var TERMINAL_STATES = /* @__PURE__ */ new Set(["completed", "failed", "canceled", "rejected", "auth-required"]);
|
|
336
|
+
var DEFAULT_MAX_STREAM_EVENTS = 1e4;
|
|
337
|
+
var A2AAdapter = class extends SimpleAdapter {
|
|
338
|
+
remoteUrl;
|
|
339
|
+
authHeaders;
|
|
340
|
+
streaming;
|
|
341
|
+
clientFactory;
|
|
342
|
+
maxStreamEvents;
|
|
343
|
+
logger;
|
|
344
|
+
client = null;
|
|
345
|
+
contexts = /* @__PURE__ */ new Map();
|
|
346
|
+
tasks = /* @__PURE__ */ new Map();
|
|
347
|
+
taskSenders = /* @__PURE__ */ new Map();
|
|
348
|
+
constructor(options) {
|
|
349
|
+
super({
|
|
350
|
+
historyConverter: new A2AHistoryConverter()
|
|
351
|
+
});
|
|
352
|
+
this.remoteUrl = options.remoteUrl;
|
|
353
|
+
this.authHeaders = buildA2AAuthHeaders(options.auth);
|
|
354
|
+
this.streaming = options.streaming ?? true;
|
|
355
|
+
this.clientFactory = options.clientFactory;
|
|
356
|
+
this.maxStreamEvents = normalizeMaxStreamEvents(options.maxStreamEvents);
|
|
357
|
+
this.logger = options.logger ?? new NoopLogger();
|
|
358
|
+
}
|
|
359
|
+
async onStarted(agentName, agentDescription) {
|
|
360
|
+
await super.onStarted(agentName, agentDescription);
|
|
361
|
+
this.client = await this.createClient();
|
|
362
|
+
}
|
|
363
|
+
async onMessage(message, tools, history, _participantsMessage, _contactsMessage, context) {
|
|
364
|
+
const client = this.client ?? await this.createClient();
|
|
365
|
+
this.client = client;
|
|
366
|
+
if (context.isSessionBootstrap) {
|
|
367
|
+
await this.rehydrateFromHistory(context.roomId, history, client);
|
|
368
|
+
}
|
|
369
|
+
const request = this.toSendParams(message, context.roomId);
|
|
370
|
+
try {
|
|
371
|
+
if (this.streaming) {
|
|
372
|
+
let streamEventCount = 0;
|
|
373
|
+
for await (const event of client.sendMessageStream(request)) {
|
|
374
|
+
streamEventCount += 1;
|
|
375
|
+
if (streamEventCount > this.maxStreamEvents) {
|
|
376
|
+
throw new Error(`A2A stream exceeded maximum event limit (${this.maxStreamEvents})`);
|
|
377
|
+
}
|
|
378
|
+
try {
|
|
379
|
+
await this.handleEvent(event, tools, context.roomId, message.senderId);
|
|
380
|
+
} catch (handleError) {
|
|
381
|
+
this.logger.error("A2A stream event handling failed; continuing stream", {
|
|
382
|
+
roomId: context.roomId,
|
|
383
|
+
remoteUrl: this.remoteUrl,
|
|
384
|
+
streamEventCount,
|
|
385
|
+
error: handleError
|
|
386
|
+
});
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
return;
|
|
390
|
+
}
|
|
391
|
+
const response = await client.sendMessage(request);
|
|
392
|
+
await this.handleEvent(response, tools, context.roomId, message.senderId);
|
|
393
|
+
} catch (error) {
|
|
394
|
+
const errorMessage = asErrorMessage(error);
|
|
395
|
+
this.logger.error("A2A adapter request failed", {
|
|
396
|
+
roomId: context.roomId,
|
|
397
|
+
remoteUrl: this.remoteUrl,
|
|
398
|
+
error
|
|
399
|
+
});
|
|
400
|
+
try {
|
|
401
|
+
await tools.sendEvent(`A2A agent error: ${errorMessage}`, "error", {
|
|
402
|
+
a2a_error: errorMessage
|
|
403
|
+
});
|
|
404
|
+
} catch (eventError) {
|
|
405
|
+
this.logger.warn("A2A adapter failed to emit error event", {
|
|
406
|
+
roomId: context.roomId,
|
|
407
|
+
remoteUrl: this.remoteUrl,
|
|
408
|
+
error: eventError
|
|
409
|
+
});
|
|
410
|
+
}
|
|
411
|
+
throw error instanceof Error ? error : new Error(errorMessage);
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
async onCleanup(roomId) {
|
|
415
|
+
this.contexts.delete(roomId);
|
|
416
|
+
this.tasks.delete(roomId);
|
|
417
|
+
for (const key of this.taskSenders.keys()) {
|
|
418
|
+
if (key.startsWith(`${roomId}:`)) {
|
|
419
|
+
this.taskSenders.delete(key);
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
async handleEvent(rawEvent, tools, roomId, senderId) {
|
|
424
|
+
const event = unwrapResult(rawEvent);
|
|
425
|
+
if (!event) {
|
|
426
|
+
return;
|
|
427
|
+
}
|
|
428
|
+
if (isMessageEvent(event)) {
|
|
429
|
+
const text = extractMessageText(event);
|
|
430
|
+
if (text) {
|
|
431
|
+
await tools.sendMessage(text, [{ id: senderId }]);
|
|
432
|
+
}
|
|
433
|
+
return;
|
|
434
|
+
}
|
|
435
|
+
if (isTaskEvent(event)) {
|
|
436
|
+
await this.handleTaskEvent(event, tools, roomId, senderId);
|
|
437
|
+
return;
|
|
438
|
+
}
|
|
439
|
+
if (isStatusUpdateEvent(event)) {
|
|
440
|
+
await this.handleStatusUpdateEvent(event, tools, roomId, senderId);
|
|
441
|
+
return;
|
|
442
|
+
}
|
|
443
|
+
if (isArtifactUpdateEvent(event)) {
|
|
444
|
+
const text = extractArtifactText(event.artifact);
|
|
445
|
+
if (text) {
|
|
446
|
+
await tools.sendMessage(text, [{ id: senderId }]);
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
async handleTaskEvent(task, tools, roomId, senderId) {
|
|
451
|
+
const state = normalizeState(task.status.state);
|
|
452
|
+
const key = roomTaskKey(roomId, task.id);
|
|
453
|
+
const sender = this.trackTask({
|
|
454
|
+
roomId,
|
|
455
|
+
key,
|
|
456
|
+
taskId: task.id,
|
|
457
|
+
contextId: task.contextId,
|
|
458
|
+
senderId
|
|
459
|
+
});
|
|
460
|
+
await this.handleStateTransition({
|
|
461
|
+
state,
|
|
462
|
+
statusMessage: task.status.message,
|
|
463
|
+
completedMessage: extractTaskResponse(task),
|
|
464
|
+
tools,
|
|
465
|
+
sender,
|
|
466
|
+
contextId: task.contextId,
|
|
467
|
+
taskId: task.id,
|
|
468
|
+
roomId,
|
|
469
|
+
key
|
|
470
|
+
});
|
|
471
|
+
}
|
|
472
|
+
async handleStatusUpdateEvent(event, tools, roomId, senderId) {
|
|
473
|
+
const state = normalizeState(event.status.state);
|
|
474
|
+
const key = roomTaskKey(roomId, event.taskId);
|
|
475
|
+
const sender = this.trackTask({
|
|
476
|
+
roomId,
|
|
477
|
+
key,
|
|
478
|
+
taskId: event.taskId,
|
|
479
|
+
contextId: event.contextId,
|
|
480
|
+
senderId
|
|
481
|
+
});
|
|
482
|
+
await this.handleStateTransition({
|
|
483
|
+
state,
|
|
484
|
+
statusMessage: event.status.message,
|
|
485
|
+
completedMessage: extractMessageText(event.status.message),
|
|
486
|
+
tools,
|
|
487
|
+
sender,
|
|
488
|
+
contextId: event.contextId,
|
|
489
|
+
taskId: event.taskId,
|
|
490
|
+
roomId,
|
|
491
|
+
key
|
|
492
|
+
});
|
|
493
|
+
}
|
|
494
|
+
trackTask(input) {
|
|
495
|
+
const sender = this.taskSenders.get(input.key) ?? { id: input.senderId };
|
|
496
|
+
this.taskSenders.set(input.key, sender);
|
|
497
|
+
this.tasks.set(input.roomId, input.taskId);
|
|
498
|
+
if (input.contextId) {
|
|
499
|
+
this.contexts.set(input.roomId, input.contextId);
|
|
500
|
+
}
|
|
501
|
+
return sender;
|
|
502
|
+
}
|
|
503
|
+
clearTaskTracking(key, roomId) {
|
|
504
|
+
this.taskSenders.delete(key);
|
|
505
|
+
this.tasks.delete(roomId);
|
|
506
|
+
}
|
|
507
|
+
async handleStateTransition(input) {
|
|
508
|
+
if (input.state === "working") {
|
|
509
|
+
const thought = extractMessageText(input.statusMessage);
|
|
510
|
+
if (thought) {
|
|
511
|
+
await input.tools.sendEvent(thought, "thought");
|
|
512
|
+
}
|
|
513
|
+
return;
|
|
514
|
+
}
|
|
515
|
+
if (input.state === "input-required") {
|
|
516
|
+
const text = extractMessageText(input.statusMessage) ?? "Please provide more information.";
|
|
517
|
+
await input.tools.sendMessage(text, [input.sender]);
|
|
518
|
+
await this.emitTaskEvent(input.tools, input.contextId, input.taskId, input.state);
|
|
519
|
+
return;
|
|
520
|
+
}
|
|
521
|
+
if (input.state === "completed") {
|
|
522
|
+
if (input.completedMessage) {
|
|
523
|
+
await input.tools.sendMessage(input.completedMessage, [input.sender]);
|
|
524
|
+
}
|
|
525
|
+
await this.emitTaskEvent(input.tools, input.contextId, input.taskId, input.state);
|
|
526
|
+
this.clearTaskTracking(input.key, input.roomId);
|
|
527
|
+
return;
|
|
528
|
+
}
|
|
529
|
+
if (TERMINAL_STATES.has(input.state)) {
|
|
530
|
+
const text = extractMessageText(input.statusMessage) ?? `A2A task ${input.state}`;
|
|
531
|
+
await input.tools.sendEvent(text, "error", { a2a_state: input.state });
|
|
532
|
+
await this.emitTaskEvent(input.tools, input.contextId, input.taskId, input.state);
|
|
533
|
+
this.clearTaskTracking(input.key, input.roomId);
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
async emitTaskEvent(tools, contextId, taskId, state) {
|
|
537
|
+
await tools.sendEvent(`A2A task ${state}`, "task", {
|
|
538
|
+
a2a_context_id: contextId ?? null,
|
|
539
|
+
a2a_task_id: taskId,
|
|
540
|
+
a2a_task_state: state
|
|
541
|
+
});
|
|
542
|
+
}
|
|
543
|
+
toSendParams(message, roomId) {
|
|
544
|
+
const contextId = this.contexts.get(roomId);
|
|
545
|
+
const taskId = this.tasks.get(roomId);
|
|
546
|
+
return {
|
|
547
|
+
message: {
|
|
548
|
+
kind: "message",
|
|
549
|
+
messageId: randomUUID(),
|
|
550
|
+
role: "user",
|
|
551
|
+
parts: [{ kind: "text", text: message.content }],
|
|
552
|
+
...contextId ? { contextId } : {},
|
|
553
|
+
...taskId ? { taskId } : {}
|
|
554
|
+
}
|
|
555
|
+
};
|
|
556
|
+
}
|
|
557
|
+
async rehydrateFromHistory(roomId, state, client) {
|
|
558
|
+
if (state.contextId) {
|
|
559
|
+
this.contexts.set(roomId, state.contextId);
|
|
560
|
+
}
|
|
561
|
+
const normalizedState = normalizeState(state.taskState);
|
|
562
|
+
if (!state.taskId || TERMINAL_STATES.has(normalizedState) || !client.resubscribeTask) {
|
|
563
|
+
return;
|
|
564
|
+
}
|
|
565
|
+
try {
|
|
566
|
+
let resubEventCount = 0;
|
|
567
|
+
for await (const raw of client.resubscribeTask({ id: state.taskId })) {
|
|
568
|
+
resubEventCount += 1;
|
|
569
|
+
if (resubEventCount > this.maxStreamEvents) {
|
|
570
|
+
this.logger.warn("A2A task resubscribe exceeded event limit; continuing with fresh task", {
|
|
571
|
+
roomId,
|
|
572
|
+
taskId: state.taskId,
|
|
573
|
+
maxStreamEvents: this.maxStreamEvents
|
|
574
|
+
});
|
|
575
|
+
break;
|
|
576
|
+
}
|
|
577
|
+
const event = unwrapResult(raw);
|
|
578
|
+
if (!event) {
|
|
579
|
+
continue;
|
|
580
|
+
}
|
|
581
|
+
if (isTaskEvent(event)) {
|
|
582
|
+
const eventState = normalizeState(event.status.state);
|
|
583
|
+
if (event.contextId) {
|
|
584
|
+
this.contexts.set(roomId, event.contextId);
|
|
585
|
+
}
|
|
586
|
+
if (!TERMINAL_STATES.has(eventState)) {
|
|
587
|
+
this.tasks.set(roomId, state.taskId);
|
|
588
|
+
}
|
|
589
|
+
break;
|
|
590
|
+
}
|
|
591
|
+
if (isStatusUpdateEvent(event)) {
|
|
592
|
+
const eventState = normalizeState(event.status.state);
|
|
593
|
+
if (event.contextId) {
|
|
594
|
+
this.contexts.set(roomId, event.contextId);
|
|
595
|
+
}
|
|
596
|
+
if (!TERMINAL_STATES.has(eventState)) {
|
|
597
|
+
this.tasks.set(roomId, state.taskId);
|
|
598
|
+
}
|
|
599
|
+
break;
|
|
600
|
+
}
|
|
601
|
+
}
|
|
602
|
+
} catch (error) {
|
|
603
|
+
this.logger.warn("A2A task resubscribe failed; continuing with fresh task", {
|
|
604
|
+
roomId,
|
|
605
|
+
taskId: state.taskId,
|
|
606
|
+
error
|
|
607
|
+
});
|
|
608
|
+
}
|
|
609
|
+
}
|
|
610
|
+
async createClient() {
|
|
611
|
+
const factory = this.clientFactory ?? await loadDefaultA2AClientFactory();
|
|
612
|
+
return factory({
|
|
613
|
+
remoteUrl: this.remoteUrl,
|
|
614
|
+
authHeaders: this.authHeaders
|
|
615
|
+
});
|
|
616
|
+
}
|
|
617
|
+
};
|
|
618
|
+
function roomTaskKey(roomId, taskId) {
|
|
619
|
+
return `${roomId}:${taskId}`;
|
|
620
|
+
}
|
|
621
|
+
function normalizeMaxStreamEvents(value) {
|
|
622
|
+
const maxStreamEvents = value ?? DEFAULT_MAX_STREAM_EVENTS;
|
|
623
|
+
if (!Number.isSafeInteger(maxStreamEvents) || maxStreamEvents < 1) {
|
|
624
|
+
throw new ValidationError("A2AAdapter `maxStreamEvents` must be a positive integer.");
|
|
625
|
+
}
|
|
626
|
+
return maxStreamEvents;
|
|
627
|
+
}
|
|
628
|
+
function normalizeState(state) {
|
|
629
|
+
if (typeof state !== "string") {
|
|
630
|
+
return "unknown";
|
|
631
|
+
}
|
|
632
|
+
const normalized = state.toLowerCase().replaceAll("_", "-");
|
|
633
|
+
if (normalized === "input-required") {
|
|
634
|
+
return "input-required";
|
|
635
|
+
}
|
|
636
|
+
if (normalized === "submitted") {
|
|
637
|
+
return "submitted";
|
|
638
|
+
}
|
|
639
|
+
if (normalized === "working") {
|
|
640
|
+
return "working";
|
|
641
|
+
}
|
|
642
|
+
if (normalized === "completed") {
|
|
643
|
+
return "completed";
|
|
644
|
+
}
|
|
645
|
+
if (normalized === "canceled") {
|
|
646
|
+
return "canceled";
|
|
647
|
+
}
|
|
648
|
+
if (normalized === "failed") {
|
|
649
|
+
return "failed";
|
|
650
|
+
}
|
|
651
|
+
if (normalized === "rejected") {
|
|
652
|
+
return "rejected";
|
|
653
|
+
}
|
|
654
|
+
if (normalized === "auth-required") {
|
|
655
|
+
return "auth-required";
|
|
656
|
+
}
|
|
657
|
+
return "unknown";
|
|
658
|
+
}
|
|
659
|
+
function extractTaskResponse(task) {
|
|
660
|
+
for (const artifact of task.artifacts ?? []) {
|
|
661
|
+
const text = extractArtifactText(artifact);
|
|
662
|
+
if (text) {
|
|
663
|
+
return text;
|
|
664
|
+
}
|
|
665
|
+
}
|
|
666
|
+
const statusText = extractMessageText(task.status.message);
|
|
667
|
+
if (statusText) {
|
|
668
|
+
return statusText;
|
|
669
|
+
}
|
|
670
|
+
for (let index = (task.history?.length ?? 0) - 1; index >= 0; index -= 1) {
|
|
671
|
+
const message = task.history?.[index];
|
|
672
|
+
if (!message || message.role !== "agent") {
|
|
673
|
+
continue;
|
|
674
|
+
}
|
|
675
|
+
const text = extractMessageText(message);
|
|
676
|
+
if (text) {
|
|
677
|
+
return text;
|
|
678
|
+
}
|
|
679
|
+
}
|
|
680
|
+
return "";
|
|
681
|
+
}
|
|
682
|
+
function extractArtifactText(artifact) {
|
|
683
|
+
if (!artifact?.parts) {
|
|
684
|
+
return null;
|
|
685
|
+
}
|
|
686
|
+
return extractPartsText(artifact.parts);
|
|
687
|
+
}
|
|
688
|
+
function extractMessageText(message) {
|
|
689
|
+
if (!message?.parts) {
|
|
690
|
+
return null;
|
|
691
|
+
}
|
|
692
|
+
return extractPartsText(message.parts);
|
|
693
|
+
}
|
|
694
|
+
function extractPartsText(parts) {
|
|
695
|
+
const chunks = [];
|
|
696
|
+
for (const part of parts) {
|
|
697
|
+
const text = extractTextFromPart(part);
|
|
698
|
+
if (text) {
|
|
699
|
+
chunks.push(text);
|
|
700
|
+
}
|
|
701
|
+
}
|
|
702
|
+
if (chunks.length === 0) {
|
|
703
|
+
return null;
|
|
704
|
+
}
|
|
705
|
+
return chunks.join("\n");
|
|
706
|
+
}
|
|
707
|
+
function extractTextFromPart(part) {
|
|
708
|
+
if (!part) {
|
|
709
|
+
return null;
|
|
710
|
+
}
|
|
711
|
+
if (typeof part.text === "string" && part.text.length > 0) {
|
|
712
|
+
return part.text;
|
|
713
|
+
}
|
|
714
|
+
if (part.root) {
|
|
715
|
+
return extractTextFromPart(part.root);
|
|
716
|
+
}
|
|
717
|
+
return null;
|
|
718
|
+
}
|
|
719
|
+
function unwrapResult(value, depth = 0) {
|
|
720
|
+
if (!value || typeof value !== "object" || depth > 10) {
|
|
721
|
+
return null;
|
|
722
|
+
}
|
|
723
|
+
const event = value;
|
|
724
|
+
if (event.result && typeof event.result === "object") {
|
|
725
|
+
return unwrapResult(event.result, depth + 1);
|
|
726
|
+
}
|
|
727
|
+
return event;
|
|
728
|
+
}
|
|
729
|
+
function isRecord(value) {
|
|
730
|
+
return typeof value === "object" && value !== null;
|
|
731
|
+
}
|
|
732
|
+
function isOptionalString(value) {
|
|
733
|
+
return value === void 0 || typeof value === "string";
|
|
734
|
+
}
|
|
735
|
+
function isMessagePart(value) {
|
|
736
|
+
if (!isRecord(value)) {
|
|
737
|
+
return false;
|
|
738
|
+
}
|
|
739
|
+
return isOptionalString(value.kind) && isOptionalString(value.type) && isOptionalString(value.text) && (value.root === void 0 || isMessagePart(value.root));
|
|
740
|
+
}
|
|
741
|
+
function isMessageLike(value) {
|
|
742
|
+
if (!isRecord(value)) {
|
|
743
|
+
return false;
|
|
744
|
+
}
|
|
745
|
+
if (!isOptionalString(value.kind) || !isOptionalString(value.role)) {
|
|
746
|
+
return false;
|
|
747
|
+
}
|
|
748
|
+
if (!isOptionalString(value.contextId) || !isOptionalString(value.taskId) || !isOptionalString(value.messageId)) {
|
|
749
|
+
return false;
|
|
750
|
+
}
|
|
751
|
+
if (value.parts === void 0) {
|
|
752
|
+
return true;
|
|
753
|
+
}
|
|
754
|
+
return Array.isArray(value.parts) && value.parts.every((part) => isMessagePart(part));
|
|
755
|
+
}
|
|
756
|
+
function isStatusLike(value) {
|
|
757
|
+
if (!isRecord(value)) {
|
|
758
|
+
return false;
|
|
759
|
+
}
|
|
760
|
+
if (!isOptionalString(value.state)) {
|
|
761
|
+
return false;
|
|
762
|
+
}
|
|
763
|
+
return value.message === void 0 || isMessageLike(value.message);
|
|
764
|
+
}
|
|
765
|
+
function isArtifactLike(value) {
|
|
766
|
+
if (!isRecord(value)) {
|
|
767
|
+
return false;
|
|
768
|
+
}
|
|
769
|
+
if (value.parts === void 0) {
|
|
770
|
+
return true;
|
|
771
|
+
}
|
|
772
|
+
return Array.isArray(value.parts) && value.parts.every((part) => isMessagePart(part));
|
|
773
|
+
}
|
|
774
|
+
function isMessageEvent(event) {
|
|
775
|
+
return isRecord(event) && event.kind === "message" && isMessageLike(event) && Array.isArray(event.parts);
|
|
776
|
+
}
|
|
777
|
+
function isTaskEvent(event) {
|
|
778
|
+
if (!isRecord(event) || event.kind !== "task") {
|
|
779
|
+
return false;
|
|
780
|
+
}
|
|
781
|
+
return typeof event.id === "string" && isOptionalString(event.contextId) && isStatusLike(event.status) && (event.artifacts === void 0 || Array.isArray(event.artifacts) && event.artifacts.every((item) => isArtifactLike(item))) && (event.history === void 0 || Array.isArray(event.history) && event.history.every((item) => isMessageLike(item)));
|
|
782
|
+
}
|
|
783
|
+
function isStatusUpdateEvent(event) {
|
|
784
|
+
return isRecord(event) && event.kind === "status-update" && typeof event.taskId === "string" && typeof event.contextId === "string" && isStatusLike(event.status);
|
|
785
|
+
}
|
|
786
|
+
function isArtifactUpdateEvent(event) {
|
|
787
|
+
return isRecord(event) && event.kind === "artifact-update" && typeof event.taskId === "string" && typeof event.contextId === "string" && isArtifactLike(event.artifact);
|
|
788
|
+
}
|
|
789
|
+
async function loadDefaultA2AClientFactory() {
|
|
790
|
+
let module;
|
|
791
|
+
try {
|
|
792
|
+
module = await import("@a2a-js/sdk/client");
|
|
793
|
+
} catch (error) {
|
|
794
|
+
throw new UnsupportedFeatureError(
|
|
795
|
+
`A2AAdapter requires optional dependency @a2a-js/sdk. Install it with "pnpm add @a2a-js/sdk". (${asErrorMessage(error)})`
|
|
796
|
+
);
|
|
797
|
+
}
|
|
798
|
+
const ClientFactoryCtor = module.ClientFactory;
|
|
799
|
+
const JsonRpcTransportFactoryCtor = module.JsonRpcTransportFactory;
|
|
800
|
+
const RestTransportFactoryCtor = module.RestTransportFactory;
|
|
801
|
+
if (!ClientFactoryCtor || !JsonRpcTransportFactoryCtor || !RestTransportFactoryCtor) {
|
|
802
|
+
throw new UnsupportedFeatureError("Failed to initialize A2A client: missing required exports from @a2a-js/sdk/client.");
|
|
803
|
+
}
|
|
804
|
+
return async ({ remoteUrl, authHeaders }) => {
|
|
805
|
+
const fetchImpl = createAuthFetch(authHeaders);
|
|
806
|
+
const factory = new ClientFactoryCtor({
|
|
807
|
+
transports: [
|
|
808
|
+
new JsonRpcTransportFactoryCtor({ fetchImpl }),
|
|
809
|
+
new RestTransportFactoryCtor({ fetchImpl })
|
|
810
|
+
]
|
|
811
|
+
});
|
|
812
|
+
return factory.createFromUrl(remoteUrl);
|
|
813
|
+
};
|
|
814
|
+
}
|
|
815
|
+
var VALID_HEADER_NAME = /^[!#$%&'*+\-.^_`|~0-9a-zA-Z]+$/;
|
|
816
|
+
function createAuthFetch(authHeaders) {
|
|
817
|
+
if (typeof globalThis.fetch !== "function") {
|
|
818
|
+
throw new UnsupportedFeatureError("A2AAdapter requires global fetch support (Node.js 20+).");
|
|
819
|
+
}
|
|
820
|
+
const baseFetch = globalThis.fetch.bind(globalThis);
|
|
821
|
+
const validatedHeaders = Object.entries(authHeaders).filter(([key]) => {
|
|
822
|
+
if (!VALID_HEADER_NAME.test(key)) {
|
|
823
|
+
return false;
|
|
824
|
+
}
|
|
825
|
+
return true;
|
|
826
|
+
});
|
|
827
|
+
if (validatedHeaders.length === 0) {
|
|
828
|
+
return baseFetch;
|
|
829
|
+
}
|
|
830
|
+
return async (input, init) => {
|
|
831
|
+
const headers = new Headers(init?.headers ?? {});
|
|
832
|
+
for (const [key, value] of validatedHeaders) {
|
|
833
|
+
headers.set(key, value);
|
|
834
|
+
}
|
|
835
|
+
return baseFetch(input, {
|
|
836
|
+
...init,
|
|
837
|
+
headers
|
|
838
|
+
});
|
|
839
|
+
};
|
|
840
|
+
}
|
|
841
|
+
|
|
842
|
+
// src/adapters/a2a-gateway/history.ts
|
|
843
|
+
var GatewayHistoryConverter = class {
|
|
844
|
+
convert(raw) {
|
|
845
|
+
const contextToRoom = {};
|
|
846
|
+
const roomParticipants = /* @__PURE__ */ new Map();
|
|
847
|
+
for (const entry of raw) {
|
|
848
|
+
const metadata = asOptionalRecord(entry.metadata) ?? {};
|
|
849
|
+
const contextId = asNonEmptyString(metadata.gateway_context_id);
|
|
850
|
+
if (contextId) {
|
|
851
|
+
const roomId2 = asNonEmptyString(metadata.gateway_room_id) ?? asNonEmptyString(entry.room_id) ?? asNonEmptyString(entry.roomId);
|
|
852
|
+
if (roomId2) {
|
|
853
|
+
contextToRoom[contextId] = roomId2;
|
|
854
|
+
}
|
|
855
|
+
}
|
|
856
|
+
const senderType = (asNonEmptyString(entry.sender_type) ?? asNonEmptyString(entry.senderType) ?? "").trim().toLowerCase();
|
|
857
|
+
const roomId = asNonEmptyString(entry.room_id) ?? asNonEmptyString(entry.roomId);
|
|
858
|
+
const senderId = asNonEmptyString(entry.sender_id) ?? asNonEmptyString(entry.senderId);
|
|
859
|
+
if (senderType !== "agent" || !roomId || !senderId) {
|
|
860
|
+
continue;
|
|
861
|
+
}
|
|
862
|
+
const participants = roomParticipants.get(roomId) ?? /* @__PURE__ */ new Set();
|
|
863
|
+
participants.add(senderId);
|
|
864
|
+
roomParticipants.set(roomId, participants);
|
|
865
|
+
}
|
|
866
|
+
const serializedParticipants = {};
|
|
867
|
+
for (const [roomId, participants] of roomParticipants) {
|
|
868
|
+
serializedParticipants[roomId] = [...participants];
|
|
869
|
+
}
|
|
870
|
+
return {
|
|
871
|
+
contextToRoom,
|
|
872
|
+
roomParticipants: serializedParticipants
|
|
873
|
+
};
|
|
874
|
+
}
|
|
875
|
+
};
|
|
876
|
+
|
|
877
|
+
// src/adapters/a2a-gateway/server.ts
|
|
878
|
+
import { randomUUID as randomUUID3, timingSafeEqual } from "crypto";
|
|
879
|
+
|
|
880
|
+
// src/adapters/a2a-gateway/statusEvent.ts
|
|
881
|
+
import { randomUUID as randomUUID2 } from "crypto";
|
|
882
|
+
function buildStatusEvent(input) {
|
|
883
|
+
return {
|
|
884
|
+
kind: "status-update",
|
|
885
|
+
taskId: input.taskId,
|
|
886
|
+
contextId: input.contextId,
|
|
887
|
+
final: input.final,
|
|
888
|
+
status: {
|
|
889
|
+
state: input.state,
|
|
890
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
891
|
+
message: {
|
|
892
|
+
kind: "message",
|
|
893
|
+
messageId: randomUUID2(),
|
|
894
|
+
role: "agent",
|
|
895
|
+
taskId: input.taskId,
|
|
896
|
+
contextId: input.contextId,
|
|
897
|
+
parts: input.text ? [
|
|
898
|
+
{
|
|
899
|
+
kind: "text",
|
|
900
|
+
text: input.text
|
|
901
|
+
}
|
|
902
|
+
] : []
|
|
903
|
+
}
|
|
904
|
+
},
|
|
905
|
+
metadata: input.metadata
|
|
906
|
+
};
|
|
907
|
+
}
|
|
908
|
+
|
|
909
|
+
// src/adapters/a2a-gateway/server.ts
|
|
910
|
+
var GatewayPeerExecutor = class {
|
|
911
|
+
peer;
|
|
912
|
+
onRequest;
|
|
913
|
+
onCancel;
|
|
914
|
+
constructor(options) {
|
|
915
|
+
this.peer = options.peer;
|
|
916
|
+
this.onRequest = options.onRequest;
|
|
917
|
+
this.onCancel = options.onCancel;
|
|
918
|
+
}
|
|
919
|
+
async execute(requestContext, eventBus) {
|
|
920
|
+
const taskId = asNonEmptyString(requestContext.taskId) ?? randomUUID3();
|
|
921
|
+
const contextId = asNonEmptyString(requestContext.contextId) ?? randomUUID3();
|
|
922
|
+
const message = asA2AMessage(requestContext.userMessage, contextId, taskId);
|
|
923
|
+
try {
|
|
924
|
+
for await (const event of this.onRequest({
|
|
925
|
+
peerId: this.peer.id,
|
|
926
|
+
peerSlug: this.peer.slug,
|
|
927
|
+
taskId,
|
|
928
|
+
contextId,
|
|
929
|
+
message
|
|
930
|
+
})) {
|
|
931
|
+
eventBus.publish(event);
|
|
932
|
+
if (event.final) {
|
|
933
|
+
break;
|
|
934
|
+
}
|
|
935
|
+
}
|
|
936
|
+
} catch (error) {
|
|
937
|
+
eventBus.publish(
|
|
938
|
+
buildStatusEvent({
|
|
939
|
+
taskId,
|
|
940
|
+
contextId,
|
|
941
|
+
state: "failed",
|
|
942
|
+
final: true,
|
|
943
|
+
text: "Peer request failed.",
|
|
944
|
+
metadata: buildGatewayExecutionFailureMetadata(error)
|
|
945
|
+
})
|
|
946
|
+
);
|
|
947
|
+
} finally {
|
|
948
|
+
eventBus.finished();
|
|
949
|
+
}
|
|
950
|
+
}
|
|
951
|
+
async cancelTask(taskId, eventBus) {
|
|
952
|
+
const request = {
|
|
953
|
+
peerId: this.peer.id,
|
|
954
|
+
peerSlug: this.peer.slug,
|
|
955
|
+
taskId
|
|
956
|
+
};
|
|
957
|
+
await this.onCancel?.(request);
|
|
958
|
+
eventBus.publish(
|
|
959
|
+
buildStatusEvent({
|
|
960
|
+
taskId,
|
|
961
|
+
contextId: randomUUID3(),
|
|
962
|
+
state: "canceled",
|
|
963
|
+
final: true,
|
|
964
|
+
text: "Task canceled."
|
|
965
|
+
})
|
|
966
|
+
);
|
|
967
|
+
eventBus.finished();
|
|
968
|
+
}
|
|
969
|
+
};
|
|
970
|
+
var GatewayServer = class {
|
|
971
|
+
options;
|
|
972
|
+
server = null;
|
|
973
|
+
started = false;
|
|
974
|
+
constructor(options) {
|
|
975
|
+
this.options = options;
|
|
976
|
+
}
|
|
977
|
+
async start() {
|
|
978
|
+
if (this.started) {
|
|
979
|
+
return;
|
|
980
|
+
}
|
|
981
|
+
assertGatewayAuthPolicy(
|
|
982
|
+
this.options.host,
|
|
983
|
+
this.options.authToken,
|
|
984
|
+
this.options.allowUnauthenticatedLoopback ?? false
|
|
985
|
+
);
|
|
986
|
+
const modules = await this.loadModules();
|
|
987
|
+
const gatewayAuthMiddleware = createGatewayAuthMiddleware(this.options.authToken);
|
|
988
|
+
const app = this.createApp(modules, gatewayAuthMiddleware);
|
|
989
|
+
this.registerPeerRoutes(app, modules, gatewayAuthMiddleware);
|
|
990
|
+
this.server = await listen(app, this.options.port, this.options.host);
|
|
991
|
+
this.started = true;
|
|
992
|
+
}
|
|
993
|
+
async stop() {
|
|
994
|
+
if (!this.server) {
|
|
995
|
+
return;
|
|
996
|
+
}
|
|
997
|
+
const server = this.server;
|
|
998
|
+
this.server = null;
|
|
999
|
+
this.started = false;
|
|
1000
|
+
await new Promise((resolve, reject) => {
|
|
1001
|
+
server.close((error) => {
|
|
1002
|
+
if (error) {
|
|
1003
|
+
reject(error);
|
|
1004
|
+
return;
|
|
1005
|
+
}
|
|
1006
|
+
resolve();
|
|
1007
|
+
});
|
|
1008
|
+
});
|
|
1009
|
+
}
|
|
1010
|
+
async loadModules() {
|
|
1011
|
+
const modulesLoader = this.options.loadModules;
|
|
1012
|
+
return modulesLoader ? modulesLoader() : loadA2AServerModules();
|
|
1013
|
+
}
|
|
1014
|
+
createApp(modules, gatewayAuthMiddleware) {
|
|
1015
|
+
const app = modules.createExpressApp();
|
|
1016
|
+
app.use(createSecurityHeadersMiddleware());
|
|
1017
|
+
app.use(modules.createExpressApp.json({ limit: "1mb" }));
|
|
1018
|
+
app.use("/peers", gatewayAuthMiddleware);
|
|
1019
|
+
app.get("/peers", (_request, response) => {
|
|
1020
|
+
const peers = [...this.options.peersBySlug.values()].map((peer) => ({
|
|
1021
|
+
slug: peer.slug,
|
|
1022
|
+
id: peer.id,
|
|
1023
|
+
name: peer.name,
|
|
1024
|
+
description: peer.description
|
|
1025
|
+
}));
|
|
1026
|
+
response.json({
|
|
1027
|
+
peers,
|
|
1028
|
+
count: peers.length
|
|
1029
|
+
});
|
|
1030
|
+
});
|
|
1031
|
+
return app;
|
|
1032
|
+
}
|
|
1033
|
+
registerPeerRoutes(app, modules, gatewayAuthMiddleware) {
|
|
1034
|
+
for (const [slug, peer] of this.options.peersBySlug.entries()) {
|
|
1035
|
+
this.registerPeerRoute(app, modules, gatewayAuthMiddleware, slug, peer);
|
|
1036
|
+
}
|
|
1037
|
+
}
|
|
1038
|
+
registerPeerRoute(app, modules, gatewayAuthMiddleware, slug, peer) {
|
|
1039
|
+
const requestHandler = new modules.DefaultRequestHandler(
|
|
1040
|
+
buildAgentCard(peer, this.options.gatewayUrl),
|
|
1041
|
+
new modules.InMemoryTaskStore(),
|
|
1042
|
+
new GatewayPeerExecutor({
|
|
1043
|
+
peer,
|
|
1044
|
+
onRequest: this.options.onRequest,
|
|
1045
|
+
onCancel: this.options.onCancel
|
|
1046
|
+
})
|
|
1047
|
+
);
|
|
1048
|
+
const userBuilder = createUserBuilder(
|
|
1049
|
+
{
|
|
1050
|
+
authToken: this.options.authToken,
|
|
1051
|
+
allowUnauthenticatedLoopback: this.options.allowUnauthenticatedLoopback ?? false
|
|
1052
|
+
},
|
|
1053
|
+
modules
|
|
1054
|
+
);
|
|
1055
|
+
const peerBasePath = `/agents/${slug}`;
|
|
1056
|
+
app.get(
|
|
1057
|
+
`${peerBasePath}/.well-known/agent.json`,
|
|
1058
|
+
gatewayAuthMiddleware,
|
|
1059
|
+
async (_request, response) => {
|
|
1060
|
+
response.json(await requestHandler.getAgentCard());
|
|
1061
|
+
}
|
|
1062
|
+
);
|
|
1063
|
+
app.use(
|
|
1064
|
+
`${peerBasePath}/${modules.AGENT_CARD_PATH}`,
|
|
1065
|
+
gatewayAuthMiddleware,
|
|
1066
|
+
modules.agentCardHandler({
|
|
1067
|
+
agentCardProvider: requestHandler
|
|
1068
|
+
})
|
|
1069
|
+
);
|
|
1070
|
+
app.use(
|
|
1071
|
+
`${peerBasePath}/v1`,
|
|
1072
|
+
gatewayAuthMiddleware,
|
|
1073
|
+
modules.restHandler({
|
|
1074
|
+
requestHandler,
|
|
1075
|
+
userBuilder
|
|
1076
|
+
})
|
|
1077
|
+
);
|
|
1078
|
+
app.use(
|
|
1079
|
+
peerBasePath,
|
|
1080
|
+
gatewayAuthMiddleware,
|
|
1081
|
+
modules.jsonRpcHandler({
|
|
1082
|
+
requestHandler,
|
|
1083
|
+
userBuilder
|
|
1084
|
+
})
|
|
1085
|
+
);
|
|
1086
|
+
}
|
|
1087
|
+
};
|
|
1088
|
+
function createGatewayServer(options) {
|
|
1089
|
+
return new GatewayServer(options);
|
|
1090
|
+
}
|
|
1091
|
+
async function loadA2AServerModules() {
|
|
1092
|
+
const [sdk, server, expressServer, expressModule] = await Promise.all([
|
|
1093
|
+
import("@a2a-js/sdk"),
|
|
1094
|
+
import("@a2a-js/sdk/server"),
|
|
1095
|
+
import("@a2a-js/sdk/server/express"),
|
|
1096
|
+
import("express")
|
|
1097
|
+
]).catch((error) => {
|
|
1098
|
+
throw new Error(
|
|
1099
|
+
`A2AGatewayAdapter requires optional dependency @a2a-js/sdk and express. (${error instanceof Error ? error.message : String(error)})`,
|
|
1100
|
+
{ cause: error }
|
|
1101
|
+
);
|
|
1102
|
+
});
|
|
1103
|
+
const createExpressApp = expressModule.default ?? expressModule;
|
|
1104
|
+
const AGENT_CARD_PATH = asNonEmptyString(sdk.AGENT_CARD_PATH) ?? ".well-known/agent-card.json";
|
|
1105
|
+
const DefaultRequestHandler = server.DefaultRequestHandler;
|
|
1106
|
+
const InMemoryTaskStore = server.InMemoryTaskStore;
|
|
1107
|
+
const UserBuilder = expressServer.UserBuilder;
|
|
1108
|
+
const agentCardHandler = expressServer.agentCardHandler;
|
|
1109
|
+
const jsonRpcHandler = expressServer.jsonRpcHandler;
|
|
1110
|
+
const restHandler = expressServer.restHandler;
|
|
1111
|
+
if (!DefaultRequestHandler || !InMemoryTaskStore || !UserBuilder || typeof agentCardHandler !== "function" || typeof jsonRpcHandler !== "function" || typeof restHandler !== "function" || typeof createExpressApp !== "function" || typeof createExpressApp.json !== "function") {
|
|
1112
|
+
throw new Error(
|
|
1113
|
+
"Failed to initialize A2A gateway server: missing required exports from @a2a-js/sdk/server or @a2a-js/sdk/server/express."
|
|
1114
|
+
);
|
|
1115
|
+
}
|
|
1116
|
+
return {
|
|
1117
|
+
AGENT_CARD_PATH,
|
|
1118
|
+
DefaultRequestHandler,
|
|
1119
|
+
InMemoryTaskStore,
|
|
1120
|
+
UserBuilder,
|
|
1121
|
+
agentCardHandler,
|
|
1122
|
+
jsonRpcHandler,
|
|
1123
|
+
restHandler,
|
|
1124
|
+
createExpressApp
|
|
1125
|
+
};
|
|
1126
|
+
}
|
|
1127
|
+
function buildAgentCard(peer, gatewayUrl) {
|
|
1128
|
+
const url = `${gatewayUrl}/agents/${peer.slug}`;
|
|
1129
|
+
return {
|
|
1130
|
+
name: peer.name,
|
|
1131
|
+
description: peer.description,
|
|
1132
|
+
protocolVersion: "0.3.0",
|
|
1133
|
+
version: "1.0.0",
|
|
1134
|
+
url,
|
|
1135
|
+
preferredTransport: "JSONRPC",
|
|
1136
|
+
additionalInterfaces: [
|
|
1137
|
+
{
|
|
1138
|
+
transport: "JSONRPC",
|
|
1139
|
+
url
|
|
1140
|
+
},
|
|
1141
|
+
{
|
|
1142
|
+
transport: "HTTP+JSON",
|
|
1143
|
+
url: `${url}/v1`
|
|
1144
|
+
}
|
|
1145
|
+
],
|
|
1146
|
+
capabilities: {
|
|
1147
|
+
streaming: true,
|
|
1148
|
+
pushNotifications: false,
|
|
1149
|
+
stateTransitionHistory: false
|
|
1150
|
+
},
|
|
1151
|
+
skills: [
|
|
1152
|
+
{
|
|
1153
|
+
id: "default",
|
|
1154
|
+
name: peer.name,
|
|
1155
|
+
description: peer.description,
|
|
1156
|
+
tags: ["thenvoi", "gateway"]
|
|
1157
|
+
}
|
|
1158
|
+
],
|
|
1159
|
+
defaultInputModes: ["text/plain"],
|
|
1160
|
+
defaultOutputModes: ["text/plain"]
|
|
1161
|
+
};
|
|
1162
|
+
}
|
|
1163
|
+
function asA2AMessage(value, fallbackContextId, fallbackTaskId) {
|
|
1164
|
+
if (!value || typeof value !== "object") {
|
|
1165
|
+
return {
|
|
1166
|
+
kind: "message",
|
|
1167
|
+
messageId: randomUUID3(),
|
|
1168
|
+
role: "user",
|
|
1169
|
+
contextId: fallbackContextId,
|
|
1170
|
+
taskId: fallbackTaskId,
|
|
1171
|
+
parts: []
|
|
1172
|
+
};
|
|
1173
|
+
}
|
|
1174
|
+
const raw = value;
|
|
1175
|
+
const rawParts = Array.isArray(raw.parts) ? raw.parts : [];
|
|
1176
|
+
const parts = [];
|
|
1177
|
+
for (const part of rawParts) {
|
|
1178
|
+
if (!part || typeof part !== "object") {
|
|
1179
|
+
continue;
|
|
1180
|
+
}
|
|
1181
|
+
const candidate = part;
|
|
1182
|
+
parts.push({
|
|
1183
|
+
kind: asNonEmptyString(candidate.kind) ?? void 0,
|
|
1184
|
+
text: asNonEmptyString(candidate.text) ?? void 0,
|
|
1185
|
+
root: candidate.root && typeof candidate.root === "object" ? {
|
|
1186
|
+
text: asNonEmptyString(candidate.root.text) ?? void 0
|
|
1187
|
+
} : void 0
|
|
1188
|
+
});
|
|
1189
|
+
}
|
|
1190
|
+
return {
|
|
1191
|
+
kind: "message",
|
|
1192
|
+
messageId: asNonEmptyString(raw.messageId) ?? randomUUID3(),
|
|
1193
|
+
role: asNonEmptyString(raw.role) === "agent" ? "agent" : "user",
|
|
1194
|
+
contextId: asNonEmptyString(raw.contextId) ?? fallbackContextId,
|
|
1195
|
+
taskId: asNonEmptyString(raw.taskId) ?? fallbackTaskId,
|
|
1196
|
+
parts
|
|
1197
|
+
};
|
|
1198
|
+
}
|
|
1199
|
+
function listen(app, port, host) {
|
|
1200
|
+
return new Promise((resolve, reject) => {
|
|
1201
|
+
let server = null;
|
|
1202
|
+
const onListening = () => {
|
|
1203
|
+
if (server) {
|
|
1204
|
+
resolve(server);
|
|
1205
|
+
return;
|
|
1206
|
+
}
|
|
1207
|
+
queueMicrotask(() => {
|
|
1208
|
+
if (server) {
|
|
1209
|
+
resolve(server);
|
|
1210
|
+
return;
|
|
1211
|
+
}
|
|
1212
|
+
reject(new Error("Gateway server did not return an HTTP server instance."));
|
|
1213
|
+
});
|
|
1214
|
+
};
|
|
1215
|
+
server = app.listen(port, host, onListening);
|
|
1216
|
+
server.once("error", reject);
|
|
1217
|
+
});
|
|
1218
|
+
}
|
|
1219
|
+
function assertGatewayAuthPolicy(host, authToken, allowUnauthenticatedLoopback = false) {
|
|
1220
|
+
if (authToken) {
|
|
1221
|
+
return;
|
|
1222
|
+
}
|
|
1223
|
+
if (allowUnauthenticatedLoopback && isLoopbackHost(host)) {
|
|
1224
|
+
return;
|
|
1225
|
+
}
|
|
1226
|
+
throw new Error(
|
|
1227
|
+
"A2A gateway authToken is required unless allowUnauthenticatedLoopback is explicitly enabled on a loopback host."
|
|
1228
|
+
);
|
|
1229
|
+
}
|
|
1230
|
+
function isLoopbackHost(host) {
|
|
1231
|
+
const normalized = host.trim().toLowerCase();
|
|
1232
|
+
return normalized === "127.0.0.1" || normalized === "::1" || normalized === "localhost";
|
|
1233
|
+
}
|
|
1234
|
+
function createSecurityHeadersMiddleware() {
|
|
1235
|
+
return (_request, response, next) => {
|
|
1236
|
+
response.setHeader("Cache-Control", "no-store");
|
|
1237
|
+
response.setHeader("Referrer-Policy", "no-referrer");
|
|
1238
|
+
response.setHeader("X-Content-Type-Options", "nosniff");
|
|
1239
|
+
response.setHeader("X-Frame-Options", "DENY");
|
|
1240
|
+
next();
|
|
1241
|
+
};
|
|
1242
|
+
}
|
|
1243
|
+
function createGatewayAuthMiddleware(authToken) {
|
|
1244
|
+
if (!authToken) {
|
|
1245
|
+
return (_request, _response, next) => {
|
|
1246
|
+
next();
|
|
1247
|
+
};
|
|
1248
|
+
}
|
|
1249
|
+
return (request, response, next) => {
|
|
1250
|
+
if (verifyBearerAuthorization(request.headers, authToken)) {
|
|
1251
|
+
next();
|
|
1252
|
+
return;
|
|
1253
|
+
}
|
|
1254
|
+
response.status(401).json({
|
|
1255
|
+
error: "Unauthorized"
|
|
1256
|
+
});
|
|
1257
|
+
};
|
|
1258
|
+
}
|
|
1259
|
+
function createUserBuilder(options, modules) {
|
|
1260
|
+
const authToken = options.authToken;
|
|
1261
|
+
if (!authToken) {
|
|
1262
|
+
if (!options.allowUnauthenticatedLoopback) {
|
|
1263
|
+
throw new Error(
|
|
1264
|
+
"A2A gateway authToken is required unless allowUnauthenticatedLoopback is explicitly enabled on a loopback host."
|
|
1265
|
+
);
|
|
1266
|
+
}
|
|
1267
|
+
return async (request) => {
|
|
1268
|
+
const user = await modules.UserBuilder.noAuthentication(request);
|
|
1269
|
+
return {
|
|
1270
|
+
id: asNonEmptyString(user?.id) ?? "loopback-anonymous",
|
|
1271
|
+
authType: "none"
|
|
1272
|
+
};
|
|
1273
|
+
};
|
|
1274
|
+
}
|
|
1275
|
+
return async (request) => {
|
|
1276
|
+
if (!verifyBearerAuthorization(request.headers, authToken)) {
|
|
1277
|
+
throw new Error("Unauthorized");
|
|
1278
|
+
}
|
|
1279
|
+
return {
|
|
1280
|
+
id: "gateway-bearer",
|
|
1281
|
+
authType: "bearer"
|
|
1282
|
+
};
|
|
1283
|
+
};
|
|
1284
|
+
}
|
|
1285
|
+
function getAuthorizationHeader(headers) {
|
|
1286
|
+
if (!headers) {
|
|
1287
|
+
return null;
|
|
1288
|
+
}
|
|
1289
|
+
const value = headers.authorization ?? headers.Authorization;
|
|
1290
|
+
if (Array.isArray(value)) {
|
|
1291
|
+
return value[0] ?? null;
|
|
1292
|
+
}
|
|
1293
|
+
return typeof value === "string" ? value : null;
|
|
1294
|
+
}
|
|
1295
|
+
function verifyBearerAuthorization(headers, authToken) {
|
|
1296
|
+
const authorization = getAuthorizationHeader(headers);
|
|
1297
|
+
if (!authorization) {
|
|
1298
|
+
return false;
|
|
1299
|
+
}
|
|
1300
|
+
return safeHeaderEquals(authorization, `Bearer ${authToken}`);
|
|
1301
|
+
}
|
|
1302
|
+
function buildGatewayExecutionFailureMetadata(error) {
|
|
1303
|
+
return {
|
|
1304
|
+
error_type: error instanceof Error ? error.name : "UnknownError",
|
|
1305
|
+
error_message: sanitizeGatewayErrorMessage(error)
|
|
1306
|
+
};
|
|
1307
|
+
}
|
|
1308
|
+
function sanitizeGatewayErrorMessage(error) {
|
|
1309
|
+
const rawMessage = error instanceof Error ? error.message : String(error);
|
|
1310
|
+
const trimmed = rawMessage.trim();
|
|
1311
|
+
if (!trimmed) {
|
|
1312
|
+
return "Unknown error";
|
|
1313
|
+
}
|
|
1314
|
+
const withBearerRedaction = trimmed.replace(/Bearer\s+[^\s,;]+/gi, "Bearer [REDACTED]").replace(/(token|authorization|api[_-]?key)\s*[:=]\s*[^\s,;]+/gi, "$1=[REDACTED]");
|
|
1315
|
+
const maxLength = 240;
|
|
1316
|
+
if (withBearerRedaction.length <= maxLength) {
|
|
1317
|
+
return withBearerRedaction;
|
|
1318
|
+
}
|
|
1319
|
+
return `${withBearerRedaction.slice(0, maxLength - 3)}...`;
|
|
1320
|
+
}
|
|
1321
|
+
function safeHeaderEquals(left, right) {
|
|
1322
|
+
const leftBuffer = Buffer.from(left);
|
|
1323
|
+
const rightBuffer = Buffer.from(right);
|
|
1324
|
+
if (leftBuffer.length !== rightBuffer.length) {
|
|
1325
|
+
return false;
|
|
1326
|
+
}
|
|
1327
|
+
return timingSafeEqual(leftBuffer, rightBuffer);
|
|
1328
|
+
}
|
|
1329
|
+
|
|
1330
|
+
// src/adapters/a2a-gateway/A2AGatewayAdapter.ts
|
|
1331
|
+
import { randomUUID as randomUUID4 } from "crypto";
|
|
1332
|
+
var DEFAULT_HOST = "127.0.0.1";
|
|
1333
|
+
var DEFAULT_PORT = 1e4;
|
|
1334
|
+
var DEFAULT_GATEWAY_URL = `http://localhost:${DEFAULT_PORT}`;
|
|
1335
|
+
var DEFAULT_RESPONSE_TIMEOUT_MS = 12e4;
|
|
1336
|
+
var DEFAULT_PEER_PAGE_SIZE = 100;
|
|
1337
|
+
var DEFAULT_MAX_PEER_PAGES = 25;
|
|
1338
|
+
var A2AGatewayAdapter = class extends SimpleAdapter {
|
|
1339
|
+
thenvoiRest;
|
|
1340
|
+
gatewayUrl;
|
|
1341
|
+
host;
|
|
1342
|
+
port;
|
|
1343
|
+
authToken;
|
|
1344
|
+
allowUnauthenticatedLoopback;
|
|
1345
|
+
responseTimeoutMs;
|
|
1346
|
+
peerPageSize;
|
|
1347
|
+
maxPeerPages;
|
|
1348
|
+
serverFactory;
|
|
1349
|
+
peersBySlug = /* @__PURE__ */ new Map();
|
|
1350
|
+
peersById = /* @__PURE__ */ new Map();
|
|
1351
|
+
contextToRoom = /* @__PURE__ */ new Map();
|
|
1352
|
+
roomParticipants = /* @__PURE__ */ new Map();
|
|
1353
|
+
pendingByRoom = /* @__PURE__ */ new Map();
|
|
1354
|
+
pendingByTask = /* @__PURE__ */ new Map();
|
|
1355
|
+
server = null;
|
|
1356
|
+
constructor(options) {
|
|
1357
|
+
super({
|
|
1358
|
+
historyConverter: new GatewayHistoryConverter()
|
|
1359
|
+
});
|
|
1360
|
+
this.thenvoiRest = options.thenvoiRest;
|
|
1361
|
+
this.gatewayUrl = options.gatewayUrl ?? DEFAULT_GATEWAY_URL;
|
|
1362
|
+
this.host = options.host ?? DEFAULT_HOST;
|
|
1363
|
+
this.port = options.port ?? DEFAULT_PORT;
|
|
1364
|
+
this.authToken = options.authToken;
|
|
1365
|
+
this.allowUnauthenticatedLoopback = options.allowUnauthenticatedLoopback ?? false;
|
|
1366
|
+
this.responseTimeoutMs = options.responseTimeoutMs ?? DEFAULT_RESPONSE_TIMEOUT_MS;
|
|
1367
|
+
this.peerPageSize = options.peerPageSize ?? DEFAULT_PEER_PAGE_SIZE;
|
|
1368
|
+
this.maxPeerPages = options.maxPeerPages ?? DEFAULT_MAX_PEER_PAGES;
|
|
1369
|
+
this.serverFactory = options.serverFactory ?? createGatewayServer;
|
|
1370
|
+
if (this.allowUnauthenticatedLoopback && !isLoopbackHost2(this.host)) {
|
|
1371
|
+
throw new ValidationError(
|
|
1372
|
+
"A2AGatewayAdapter `allowUnauthenticatedLoopback` may only be enabled for loopback hosts."
|
|
1373
|
+
);
|
|
1374
|
+
}
|
|
1375
|
+
}
|
|
1376
|
+
async onStarted(agentName, agentDescription) {
|
|
1377
|
+
await super.onStarted(agentName, agentDescription);
|
|
1378
|
+
await this.refreshPeers();
|
|
1379
|
+
this.server = this.serverFactory({
|
|
1380
|
+
peersBySlug: this.peersBySlug,
|
|
1381
|
+
peersById: this.peersById,
|
|
1382
|
+
gatewayUrl: this.gatewayUrl,
|
|
1383
|
+
host: this.host,
|
|
1384
|
+
port: this.port,
|
|
1385
|
+
authToken: this.authToken,
|
|
1386
|
+
allowUnauthenticatedLoopback: this.allowUnauthenticatedLoopback,
|
|
1387
|
+
onRequest: (request) => this.handleGatewayRequest(request),
|
|
1388
|
+
onCancel: async (request) => {
|
|
1389
|
+
const canonicalPeerId = this.resolveCanonicalPeerId(request);
|
|
1390
|
+
this.cancelPendingTask(request.taskId, canonicalPeerId ?? request.peerId);
|
|
1391
|
+
}
|
|
1392
|
+
});
|
|
1393
|
+
await this.server.start();
|
|
1394
|
+
}
|
|
1395
|
+
async onMessage(message, _tools, history, _participantsMessage, _contactsMessage, context) {
|
|
1396
|
+
if (context.isSessionBootstrap) {
|
|
1397
|
+
this.rehydrate(history);
|
|
1398
|
+
}
|
|
1399
|
+
const pending = this.resolvePendingForMessage(context.roomId, message);
|
|
1400
|
+
if (!pending) {
|
|
1401
|
+
return;
|
|
1402
|
+
}
|
|
1403
|
+
const event = toStatusUpdateEvent(message, pending.taskId, pending.contextId);
|
|
1404
|
+
pending.enqueue(event);
|
|
1405
|
+
if (event.final) {
|
|
1406
|
+
this.removePending(pending);
|
|
1407
|
+
}
|
|
1408
|
+
}
|
|
1409
|
+
async onCleanup(roomId) {
|
|
1410
|
+
const roomPendings = this.pendingByRoom.get(roomId);
|
|
1411
|
+
if (roomPendings && roomPendings.size > 0) {
|
|
1412
|
+
for (const pending of roomPendings.values()) {
|
|
1413
|
+
pending.queue.cancel();
|
|
1414
|
+
}
|
|
1415
|
+
roomPendings.clear();
|
|
1416
|
+
this.pendingByRoom.delete(roomId);
|
|
1417
|
+
}
|
|
1418
|
+
for (const [taskId, pending] of this.pendingByTask) {
|
|
1419
|
+
if (pending.roomId === roomId) {
|
|
1420
|
+
this.pendingByTask.delete(taskId);
|
|
1421
|
+
}
|
|
1422
|
+
}
|
|
1423
|
+
this.forgetRoom(roomId);
|
|
1424
|
+
}
|
|
1425
|
+
async stopGatewayServer() {
|
|
1426
|
+
if (!this.server) {
|
|
1427
|
+
return;
|
|
1428
|
+
}
|
|
1429
|
+
const server = this.server;
|
|
1430
|
+
this.server = null;
|
|
1431
|
+
await server.stop();
|
|
1432
|
+
}
|
|
1433
|
+
async onRuntimeStop() {
|
|
1434
|
+
this.resetState();
|
|
1435
|
+
await this.stopGatewayServer();
|
|
1436
|
+
}
|
|
1437
|
+
async refreshPeers() {
|
|
1438
|
+
this.peersBySlug.clear();
|
|
1439
|
+
this.peersById.clear();
|
|
1440
|
+
if (!this.thenvoiRest.listPeers) {
|
|
1441
|
+
throw new UnsupportedFeatureError(
|
|
1442
|
+
"Peer listing is not available in current REST adapter"
|
|
1443
|
+
);
|
|
1444
|
+
}
|
|
1445
|
+
for (let page = 1; page <= this.maxPeerPages; page += 1) {
|
|
1446
|
+
const response = await this.thenvoiRest.listPeers({
|
|
1447
|
+
page,
|
|
1448
|
+
pageSize: this.peerPageSize,
|
|
1449
|
+
notInChat: ""
|
|
1450
|
+
});
|
|
1451
|
+
const peers = Array.isArray(response.data) ? response.data : [];
|
|
1452
|
+
for (const item of peers) {
|
|
1453
|
+
const peer = toGatewayPeer(item);
|
|
1454
|
+
if (!peer) {
|
|
1455
|
+
continue;
|
|
1456
|
+
}
|
|
1457
|
+
const slug = uniqueSlug(peer.slug, this.peersBySlug);
|
|
1458
|
+
const normalized = {
|
|
1459
|
+
...peer,
|
|
1460
|
+
slug
|
|
1461
|
+
};
|
|
1462
|
+
this.peersBySlug.set(slug, normalized);
|
|
1463
|
+
this.peersById.set(normalized.id, normalized);
|
|
1464
|
+
}
|
|
1465
|
+
if (peers.length < this.peerPageSize) {
|
|
1466
|
+
break;
|
|
1467
|
+
}
|
|
1468
|
+
}
|
|
1469
|
+
}
|
|
1470
|
+
async *handleGatewayRequest(request) {
|
|
1471
|
+
const peer = this.resolveGatewayPeer(request);
|
|
1472
|
+
if (!peer) {
|
|
1473
|
+
yield buildStatusEvent({
|
|
1474
|
+
taskId: request.taskId,
|
|
1475
|
+
contextId: request.contextId,
|
|
1476
|
+
state: "failed",
|
|
1477
|
+
final: true,
|
|
1478
|
+
text: `Peer not found: ${request.peerId}`
|
|
1479
|
+
});
|
|
1480
|
+
return;
|
|
1481
|
+
}
|
|
1482
|
+
const hadContext = request.contextId.length > 0 && this.contextToRoom.has(request.contextId);
|
|
1483
|
+
const [roomId, contextId] = await this.getOrCreateRoom(
|
|
1484
|
+
request.contextId,
|
|
1485
|
+
peer.id
|
|
1486
|
+
);
|
|
1487
|
+
const roomPendings = this.pendingByRoom.get(roomId);
|
|
1488
|
+
const activeInRoom = roomPendings?.size ?? 0;
|
|
1489
|
+
const queue = new AsyncEventQueue();
|
|
1490
|
+
const strictTaskMetadata = hadContext;
|
|
1491
|
+
const pending = {
|
|
1492
|
+
taskId: request.taskId,
|
|
1493
|
+
contextId,
|
|
1494
|
+
peerId: peer.id,
|
|
1495
|
+
peerSlug: peer.slug,
|
|
1496
|
+
roomId,
|
|
1497
|
+
strictTaskMetadata,
|
|
1498
|
+
requireTaskMetadata: strictTaskMetadata || activeInRoom > 0,
|
|
1499
|
+
queue,
|
|
1500
|
+
enqueue: (event) => {
|
|
1501
|
+
queue.enqueue(event);
|
|
1502
|
+
}
|
|
1503
|
+
};
|
|
1504
|
+
this.registerPending(pending);
|
|
1505
|
+
this.pendingByTask.set(pending.taskId, pending);
|
|
1506
|
+
yield buildStatusEvent({
|
|
1507
|
+
taskId: pending.taskId,
|
|
1508
|
+
contextId: pending.contextId,
|
|
1509
|
+
state: "working",
|
|
1510
|
+
final: false,
|
|
1511
|
+
text: `Routed request to ${peer.name}.`
|
|
1512
|
+
});
|
|
1513
|
+
try {
|
|
1514
|
+
await this.emitContextEvent(roomId, contextId);
|
|
1515
|
+
const content = extractMessageText2(request.message) ?? "";
|
|
1516
|
+
await this.thenvoiRest.createChatMessage(roomId, {
|
|
1517
|
+
content: buildMentionedContent(peer.name, content),
|
|
1518
|
+
mentions: buildMentions(peer),
|
|
1519
|
+
metadata: {
|
|
1520
|
+
gateway_context_id: contextId,
|
|
1521
|
+
gateway_room_id: roomId,
|
|
1522
|
+
gateway_task_id: request.taskId,
|
|
1523
|
+
gateway_peer_id: peer.id,
|
|
1524
|
+
gateway_peer_slug: peer.slug
|
|
1525
|
+
}
|
|
1526
|
+
});
|
|
1527
|
+
} catch (error) {
|
|
1528
|
+
this.removePending(pending);
|
|
1529
|
+
yield buildStatusEvent({
|
|
1530
|
+
taskId: pending.taskId,
|
|
1531
|
+
contextId: pending.contextId,
|
|
1532
|
+
state: "failed",
|
|
1533
|
+
final: true,
|
|
1534
|
+
text: error instanceof Error ? error.message : String(error)
|
|
1535
|
+
});
|
|
1536
|
+
return;
|
|
1537
|
+
}
|
|
1538
|
+
while (true) {
|
|
1539
|
+
const next = await pending.queue.dequeue(this.responseTimeoutMs);
|
|
1540
|
+
if (!next) {
|
|
1541
|
+
this.removePending(pending);
|
|
1542
|
+
yield buildStatusEvent({
|
|
1543
|
+
taskId: pending.taskId,
|
|
1544
|
+
contextId: pending.contextId,
|
|
1545
|
+
state: "failed",
|
|
1546
|
+
final: true,
|
|
1547
|
+
text: "Timed out waiting for a Thenvoi peer response."
|
|
1548
|
+
});
|
|
1549
|
+
return;
|
|
1550
|
+
}
|
|
1551
|
+
yield next;
|
|
1552
|
+
if (next.final) {
|
|
1553
|
+
this.removePending(pending);
|
|
1554
|
+
return;
|
|
1555
|
+
}
|
|
1556
|
+
}
|
|
1557
|
+
}
|
|
1558
|
+
cancelPendingTask(taskId, peerId) {
|
|
1559
|
+
const pending = this.pendingByTask.get(taskId);
|
|
1560
|
+
if (!pending) {
|
|
1561
|
+
return;
|
|
1562
|
+
}
|
|
1563
|
+
this.finalizePending(
|
|
1564
|
+
pending,
|
|
1565
|
+
buildStatusEvent({
|
|
1566
|
+
taskId,
|
|
1567
|
+
contextId: pending.contextId,
|
|
1568
|
+
state: "canceled",
|
|
1569
|
+
final: true,
|
|
1570
|
+
text: `Task canceled by A2A client (${peerId}).`
|
|
1571
|
+
})
|
|
1572
|
+
);
|
|
1573
|
+
}
|
|
1574
|
+
registerPending(pending) {
|
|
1575
|
+
const roomPendings = this.pendingByRoom.get(pending.roomId) ?? /* @__PURE__ */ new Map();
|
|
1576
|
+
roomPendings.set(pending.taskId, pending);
|
|
1577
|
+
this.pendingByRoom.set(pending.roomId, roomPendings);
|
|
1578
|
+
if (roomPendings.size > 1) {
|
|
1579
|
+
for (const entry of roomPendings.values()) {
|
|
1580
|
+
entry.requireTaskMetadata = true;
|
|
1581
|
+
}
|
|
1582
|
+
}
|
|
1583
|
+
}
|
|
1584
|
+
resolvePendingForMessage(roomId, message) {
|
|
1585
|
+
const roomPendings = this.pendingByRoom.get(roomId);
|
|
1586
|
+
if (!roomPendings || roomPendings.size === 0) {
|
|
1587
|
+
return null;
|
|
1588
|
+
}
|
|
1589
|
+
const metadata = message.metadata ?? {};
|
|
1590
|
+
const gatewayTaskId = asNonEmptyString(metadata.gateway_task_id);
|
|
1591
|
+
if (gatewayTaskId) {
|
|
1592
|
+
const pending = roomPendings.get(gatewayTaskId) ?? null;
|
|
1593
|
+
if (!pending || !shouldRouteToPendingTask(message, pending)) {
|
|
1594
|
+
return null;
|
|
1595
|
+
}
|
|
1596
|
+
return pending;
|
|
1597
|
+
}
|
|
1598
|
+
let matched = null;
|
|
1599
|
+
for (const pending of roomPendings.values()) {
|
|
1600
|
+
if (!shouldRouteToPendingTask(message, pending)) {
|
|
1601
|
+
continue;
|
|
1602
|
+
}
|
|
1603
|
+
if (matched) {
|
|
1604
|
+
return null;
|
|
1605
|
+
}
|
|
1606
|
+
matched = pending;
|
|
1607
|
+
}
|
|
1608
|
+
return matched;
|
|
1609
|
+
}
|
|
1610
|
+
resolveGatewayPeer(request) {
|
|
1611
|
+
const canonicalPeerId = this.resolveCanonicalPeerId(request);
|
|
1612
|
+
if (!canonicalPeerId) {
|
|
1613
|
+
return null;
|
|
1614
|
+
}
|
|
1615
|
+
return this.peersById.get(canonicalPeerId) ?? null;
|
|
1616
|
+
}
|
|
1617
|
+
resolveCanonicalPeerId(request) {
|
|
1618
|
+
const byId = this.peersById.get(request.peerId);
|
|
1619
|
+
if (byId) {
|
|
1620
|
+
return byId.id;
|
|
1621
|
+
}
|
|
1622
|
+
const legacySlug = request.peerSlug ?? request.peerId;
|
|
1623
|
+
const bySlug = this.peersBySlug.get(legacySlug);
|
|
1624
|
+
if (bySlug) {
|
|
1625
|
+
return bySlug.id;
|
|
1626
|
+
}
|
|
1627
|
+
return null;
|
|
1628
|
+
}
|
|
1629
|
+
async getOrCreateRoom(contextId, targetPeerId) {
|
|
1630
|
+
const existingRoom = this.contextToRoom.get(contextId);
|
|
1631
|
+
if (existingRoom) {
|
|
1632
|
+
const participants = this.roomParticipants.get(existingRoom) ?? /* @__PURE__ */ new Set();
|
|
1633
|
+
if (!participants.has(targetPeerId)) {
|
|
1634
|
+
await this.thenvoiRest.addChatParticipant(existingRoom, {
|
|
1635
|
+
participantId: targetPeerId,
|
|
1636
|
+
role: "member"
|
|
1637
|
+
});
|
|
1638
|
+
participants.add(targetPeerId);
|
|
1639
|
+
this.roomParticipants.set(existingRoom, participants);
|
|
1640
|
+
}
|
|
1641
|
+
return [existingRoom, contextId];
|
|
1642
|
+
}
|
|
1643
|
+
const normalizedContextId = contextId && contextId.length > 0 ? contextId : randomUUID4();
|
|
1644
|
+
const created = await this.thenvoiRest.createChat(
|
|
1645
|
+
`a2a:gateway:${normalizedContextId}`
|
|
1646
|
+
);
|
|
1647
|
+
const roomId = created.id;
|
|
1648
|
+
await this.thenvoiRest.addChatParticipant(roomId, {
|
|
1649
|
+
participantId: targetPeerId,
|
|
1650
|
+
role: "member"
|
|
1651
|
+
});
|
|
1652
|
+
this.contextToRoom.set(normalizedContextId, roomId);
|
|
1653
|
+
this.roomParticipants.set(roomId, /* @__PURE__ */ new Set([targetPeerId]));
|
|
1654
|
+
return [roomId, normalizedContextId];
|
|
1655
|
+
}
|
|
1656
|
+
async emitContextEvent(roomId, contextId) {
|
|
1657
|
+
await this.thenvoiRest.createChatEvent(roomId, {
|
|
1658
|
+
content: "A2A gateway context",
|
|
1659
|
+
messageType: "task",
|
|
1660
|
+
metadata: {
|
|
1661
|
+
gateway_context_id: contextId,
|
|
1662
|
+
gateway_room_id: roomId
|
|
1663
|
+
}
|
|
1664
|
+
});
|
|
1665
|
+
}
|
|
1666
|
+
rehydrate(history) {
|
|
1667
|
+
for (const [contextId, roomId] of Object.entries(history.contextToRoom)) {
|
|
1668
|
+
if (!this.contextToRoom.has(contextId)) {
|
|
1669
|
+
this.contextToRoom.set(contextId, roomId);
|
|
1670
|
+
}
|
|
1671
|
+
}
|
|
1672
|
+
for (const [roomId, peers] of Object.entries(history.roomParticipants)) {
|
|
1673
|
+
const existing = this.roomParticipants.get(roomId) ?? /* @__PURE__ */ new Set();
|
|
1674
|
+
for (const peer of peers) {
|
|
1675
|
+
existing.add(peer);
|
|
1676
|
+
}
|
|
1677
|
+
this.roomParticipants.set(roomId, existing);
|
|
1678
|
+
}
|
|
1679
|
+
}
|
|
1680
|
+
removePending(pending) {
|
|
1681
|
+
const roomPendings = this.pendingByRoom.get(pending.roomId);
|
|
1682
|
+
if (roomPendings) {
|
|
1683
|
+
roomPendings.delete(pending.taskId);
|
|
1684
|
+
if (roomPendings.size === 0) {
|
|
1685
|
+
this.pendingByRoom.delete(pending.roomId);
|
|
1686
|
+
} else {
|
|
1687
|
+
const hasMultiple = roomPendings.size > 1;
|
|
1688
|
+
for (const entry of roomPendings.values()) {
|
|
1689
|
+
entry.requireTaskMetadata = entry.strictTaskMetadata || hasMultiple;
|
|
1690
|
+
}
|
|
1691
|
+
}
|
|
1692
|
+
}
|
|
1693
|
+
const byTask = this.pendingByTask.get(pending.taskId);
|
|
1694
|
+
if (byTask === pending) {
|
|
1695
|
+
this.pendingByTask.delete(pending.taskId);
|
|
1696
|
+
}
|
|
1697
|
+
}
|
|
1698
|
+
finalizePending(pending, event) {
|
|
1699
|
+
pending.queue.clear();
|
|
1700
|
+
pending.enqueue(event);
|
|
1701
|
+
this.removePending(pending);
|
|
1702
|
+
}
|
|
1703
|
+
forgetRoom(roomId) {
|
|
1704
|
+
this.roomParticipants.delete(roomId);
|
|
1705
|
+
for (const [contextId, mappedRoomId] of this.contextToRoom.entries()) {
|
|
1706
|
+
if (mappedRoomId === roomId) {
|
|
1707
|
+
this.contextToRoom.delete(contextId);
|
|
1708
|
+
}
|
|
1709
|
+
}
|
|
1710
|
+
}
|
|
1711
|
+
resetState() {
|
|
1712
|
+
for (const pending of this.pendingByTask.values()) {
|
|
1713
|
+
pending.queue.cancel();
|
|
1714
|
+
}
|
|
1715
|
+
this.peersBySlug.clear();
|
|
1716
|
+
this.peersById.clear();
|
|
1717
|
+
this.contextToRoom.clear();
|
|
1718
|
+
this.roomParticipants.clear();
|
|
1719
|
+
this.pendingByRoom.clear();
|
|
1720
|
+
this.pendingByTask.clear();
|
|
1721
|
+
}
|
|
1722
|
+
};
|
|
1723
|
+
var AsyncEventQueue = class {
|
|
1724
|
+
items = [];
|
|
1725
|
+
waiters = [];
|
|
1726
|
+
cancelled = false;
|
|
1727
|
+
enqueue(item) {
|
|
1728
|
+
const waiter = this.waiters.shift();
|
|
1729
|
+
if (waiter) {
|
|
1730
|
+
waiter(item);
|
|
1731
|
+
return;
|
|
1732
|
+
}
|
|
1733
|
+
this.items.push(item);
|
|
1734
|
+
}
|
|
1735
|
+
cancel() {
|
|
1736
|
+
this.cancelled = true;
|
|
1737
|
+
this.items.length = 0;
|
|
1738
|
+
const pending = this.waiters.splice(0);
|
|
1739
|
+
for (const waiter of pending) {
|
|
1740
|
+
waiter(null);
|
|
1741
|
+
}
|
|
1742
|
+
}
|
|
1743
|
+
clear() {
|
|
1744
|
+
this.items.length = 0;
|
|
1745
|
+
}
|
|
1746
|
+
async dequeue(timeoutMs) {
|
|
1747
|
+
if (this.cancelled) {
|
|
1748
|
+
return null;
|
|
1749
|
+
}
|
|
1750
|
+
if (this.items.length > 0) {
|
|
1751
|
+
return this.items.shift() ?? null;
|
|
1752
|
+
}
|
|
1753
|
+
return new Promise((resolve) => {
|
|
1754
|
+
const waiter = (value) => {
|
|
1755
|
+
clearTimeout(timeout);
|
|
1756
|
+
resolve(value);
|
|
1757
|
+
};
|
|
1758
|
+
const timeout = setTimeout(() => {
|
|
1759
|
+
const index = this.waiters.indexOf(waiter);
|
|
1760
|
+
if (index >= 0) {
|
|
1761
|
+
this.waiters.splice(index, 1);
|
|
1762
|
+
}
|
|
1763
|
+
resolve(null);
|
|
1764
|
+
}, timeoutMs);
|
|
1765
|
+
this.waiters.push(waiter);
|
|
1766
|
+
});
|
|
1767
|
+
}
|
|
1768
|
+
};
|
|
1769
|
+
function toGatewayPeer(value) {
|
|
1770
|
+
const id = asNonEmptyString(value.id);
|
|
1771
|
+
if (!id) {
|
|
1772
|
+
return null;
|
|
1773
|
+
}
|
|
1774
|
+
const name = asNonEmptyString(value.name) ?? asNonEmptyString(value.handle) ?? id;
|
|
1775
|
+
const description = asNonEmptyString(value.description) ?? "";
|
|
1776
|
+
const handle = asNonEmptyString(value.handle);
|
|
1777
|
+
return {
|
|
1778
|
+
id,
|
|
1779
|
+
name,
|
|
1780
|
+
description,
|
|
1781
|
+
handle,
|
|
1782
|
+
slug: slugify(handle ?? name)
|
|
1783
|
+
};
|
|
1784
|
+
}
|
|
1785
|
+
function uniqueSlug(baseSlug, existing) {
|
|
1786
|
+
if (!existing.has(baseSlug)) {
|
|
1787
|
+
return baseSlug;
|
|
1788
|
+
}
|
|
1789
|
+
let index = 2;
|
|
1790
|
+
while (existing.has(`${baseSlug}-${index}`)) {
|
|
1791
|
+
index += 1;
|
|
1792
|
+
}
|
|
1793
|
+
return `${baseSlug}-${index}`;
|
|
1794
|
+
}
|
|
1795
|
+
function isLoopbackHost2(host) {
|
|
1796
|
+
const normalized = host.trim().toLowerCase();
|
|
1797
|
+
return normalized === "127.0.0.1" || normalized === "::1" || normalized === "localhost";
|
|
1798
|
+
}
|
|
1799
|
+
function slugify(input) {
|
|
1800
|
+
return input.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "") || "peer";
|
|
1801
|
+
}
|
|
1802
|
+
function buildMentionedContent(peerName, content) {
|
|
1803
|
+
const trimmed = content.trim();
|
|
1804
|
+
if (!trimmed) {
|
|
1805
|
+
return `@${peerName}`;
|
|
1806
|
+
}
|
|
1807
|
+
return `@${peerName} ${trimmed}`;
|
|
1808
|
+
}
|
|
1809
|
+
function buildMentions(peer) {
|
|
1810
|
+
if (peer.handle) {
|
|
1811
|
+
return [{ id: peer.id, handle: peer.handle }];
|
|
1812
|
+
}
|
|
1813
|
+
return [{ id: peer.id }];
|
|
1814
|
+
}
|
|
1815
|
+
function toStatusUpdateEvent(message, taskId, contextId) {
|
|
1816
|
+
const normalizedType = message.messageType.trim().toLowerCase();
|
|
1817
|
+
let state;
|
|
1818
|
+
let final = false;
|
|
1819
|
+
if (normalizedType === "error") {
|
|
1820
|
+
state = "failed";
|
|
1821
|
+
final = true;
|
|
1822
|
+
} else if (normalizedType === "text") {
|
|
1823
|
+
state = "completed";
|
|
1824
|
+
final = true;
|
|
1825
|
+
} else {
|
|
1826
|
+
state = "working";
|
|
1827
|
+
}
|
|
1828
|
+
return buildStatusEvent({
|
|
1829
|
+
taskId,
|
|
1830
|
+
contextId,
|
|
1831
|
+
state,
|
|
1832
|
+
final,
|
|
1833
|
+
text: message.content,
|
|
1834
|
+
metadata: {
|
|
1835
|
+
thenvoi_message_id: message.id,
|
|
1836
|
+
thenvoi_message_type: message.messageType,
|
|
1837
|
+
thenvoi_sender_id: message.senderId,
|
|
1838
|
+
thenvoi_room_id: message.roomId
|
|
1839
|
+
}
|
|
1840
|
+
});
|
|
1841
|
+
}
|
|
1842
|
+
function shouldRouteToPendingTask(message, pending) {
|
|
1843
|
+
const metadata = message.metadata ?? {};
|
|
1844
|
+
const gatewayTaskId = asNonEmptyString(metadata.gateway_task_id);
|
|
1845
|
+
if (gatewayTaskId) {
|
|
1846
|
+
return gatewayTaskId === pending.taskId;
|
|
1847
|
+
}
|
|
1848
|
+
if (pending.requireTaskMetadata) {
|
|
1849
|
+
return false;
|
|
1850
|
+
}
|
|
1851
|
+
const gatewayContextId = asNonEmptyString(metadata.gateway_context_id);
|
|
1852
|
+
if (gatewayContextId && gatewayContextId !== pending.contextId) {
|
|
1853
|
+
return false;
|
|
1854
|
+
}
|
|
1855
|
+
const gatewayPeerId = asNonEmptyString(metadata.gateway_peer_id);
|
|
1856
|
+
if (gatewayPeerId) {
|
|
1857
|
+
return peerMatchesPendingIdentifier(gatewayPeerId, pending);
|
|
1858
|
+
}
|
|
1859
|
+
const gatewayPeerSlug = asNonEmptyString(metadata.gateway_peer_slug);
|
|
1860
|
+
if (gatewayPeerSlug) {
|
|
1861
|
+
return gatewayPeerSlug === pending.peerSlug;
|
|
1862
|
+
}
|
|
1863
|
+
return peerMatchesPendingIdentifier(message.senderId, pending);
|
|
1864
|
+
}
|
|
1865
|
+
function peerMatchesPendingIdentifier(identifier, pending) {
|
|
1866
|
+
return identifier === pending.peerId || identifier === pending.peerSlug;
|
|
1867
|
+
}
|
|
1868
|
+
function extractMessageText2(message) {
|
|
1869
|
+
const chunks = [];
|
|
1870
|
+
for (const part of message.parts) {
|
|
1871
|
+
if (typeof part.text === "string" && part.text.trim().length > 0) {
|
|
1872
|
+
chunks.push(part.text.trim());
|
|
1873
|
+
continue;
|
|
1874
|
+
}
|
|
1875
|
+
if (part.root && typeof part.root.text === "string" && part.root.text.trim().length > 0) {
|
|
1876
|
+
chunks.push(part.root.text.trim());
|
|
1877
|
+
}
|
|
1878
|
+
}
|
|
1879
|
+
if (chunks.length === 0) {
|
|
1880
|
+
return null;
|
|
1881
|
+
}
|
|
1882
|
+
return chunks.join("\n");
|
|
1883
|
+
}
|
|
1884
|
+
|
|
1885
|
+
// src/adapters/parlant/types.ts
|
|
1886
|
+
var ParlantHistoryConverter = class {
|
|
1887
|
+
convert(raw) {
|
|
1888
|
+
const messages = [];
|
|
1889
|
+
for (const entry of raw) {
|
|
1890
|
+
const messageType = asNonEmptyString(entry.message_type) ?? "text";
|
|
1891
|
+
if (messageType !== "text") {
|
|
1892
|
+
continue;
|
|
1893
|
+
}
|
|
1894
|
+
const content = asNonEmptyString(entry.content);
|
|
1895
|
+
if (!content) {
|
|
1896
|
+
continue;
|
|
1897
|
+
}
|
|
1898
|
+
const sender = asNonEmptyString(entry.sender_name) ?? asNonEmptyString(entry.senderName) ?? "";
|
|
1899
|
+
const senderType = asNonEmptyString(entry.sender_type) ?? asNonEmptyString(entry.senderType) ?? "User";
|
|
1900
|
+
const role = (asNonEmptyString(entry.role) ?? "user").toLowerCase() === "assistant" ? "assistant" : "user";
|
|
1901
|
+
if (role === "assistant") {
|
|
1902
|
+
messages.push({
|
|
1903
|
+
role,
|
|
1904
|
+
content,
|
|
1905
|
+
sender,
|
|
1906
|
+
senderType
|
|
1907
|
+
});
|
|
1908
|
+
continue;
|
|
1909
|
+
}
|
|
1910
|
+
messages.push({
|
|
1911
|
+
role,
|
|
1912
|
+
content: sender ? `[${sender}]: ${content}` : content,
|
|
1913
|
+
sender,
|
|
1914
|
+
senderType
|
|
1915
|
+
});
|
|
1916
|
+
}
|
|
1917
|
+
return messages;
|
|
1918
|
+
}
|
|
1919
|
+
};
|
|
1920
|
+
|
|
1921
|
+
// src/adapters/shared/lazyAsyncValue.ts
|
|
1922
|
+
var DEFAULT_RETRY_BACKOFF_MS = 1e3;
|
|
1923
|
+
var LazyAsyncValue = class {
|
|
1924
|
+
load;
|
|
1925
|
+
onRejected;
|
|
1926
|
+
retryBackoffMs;
|
|
1927
|
+
value = null;
|
|
1928
|
+
pending = null;
|
|
1929
|
+
lastFailureAt = 0;
|
|
1930
|
+
constructor(options) {
|
|
1931
|
+
this.load = options.load;
|
|
1932
|
+
this.onRejected = options.onRejected;
|
|
1933
|
+
this.retryBackoffMs = options.retryBackoffMs ?? DEFAULT_RETRY_BACKOFF_MS;
|
|
1934
|
+
}
|
|
1935
|
+
get current() {
|
|
1936
|
+
return this.value;
|
|
1937
|
+
}
|
|
1938
|
+
async get() {
|
|
1939
|
+
if (this.value) {
|
|
1940
|
+
return this.value;
|
|
1941
|
+
}
|
|
1942
|
+
if (!this.pending) {
|
|
1943
|
+
if (this.lastFailureAt > 0 && Date.now() - this.lastFailureAt < this.retryBackoffMs) {
|
|
1944
|
+
throw new Error(
|
|
1945
|
+
`LazyAsyncValue load failed recently; retrying after ${this.retryBackoffMs}ms backoff.`
|
|
1946
|
+
);
|
|
1947
|
+
}
|
|
1948
|
+
this.pending = this.load().then((value) => {
|
|
1949
|
+
this.value = value;
|
|
1950
|
+
this.lastFailureAt = 0;
|
|
1951
|
+
return value;
|
|
1952
|
+
}).catch((error) => {
|
|
1953
|
+
this.pending = null;
|
|
1954
|
+
this.lastFailureAt = Date.now();
|
|
1955
|
+
this.onRejected?.(error);
|
|
1956
|
+
throw error;
|
|
1957
|
+
});
|
|
1958
|
+
}
|
|
1959
|
+
return this.pending;
|
|
1960
|
+
}
|
|
1961
|
+
clear() {
|
|
1962
|
+
this.value = null;
|
|
1963
|
+
this.pending = null;
|
|
1964
|
+
this.lastFailureAt = 0;
|
|
1965
|
+
}
|
|
1966
|
+
};
|
|
1967
|
+
|
|
1968
|
+
// src/adapters/parlant/ParlantAdapter.ts
|
|
1969
|
+
var DEFAULT_RESPONSE_TIMEOUT_SECONDS = 120;
|
|
1970
|
+
var DEFAULT_MAX_HISTORY_MESSAGES = 100;
|
|
1971
|
+
var ParlantAdapter = class extends SimpleAdapter {
|
|
1972
|
+
environment;
|
|
1973
|
+
baseUrl;
|
|
1974
|
+
agentId;
|
|
1975
|
+
apiKey;
|
|
1976
|
+
headers;
|
|
1977
|
+
systemPromptOverride;
|
|
1978
|
+
customSection;
|
|
1979
|
+
includeBaseInstructions;
|
|
1980
|
+
responseTimeoutSeconds;
|
|
1981
|
+
maxHistoryMessages;
|
|
1982
|
+
clientFactory;
|
|
1983
|
+
logger;
|
|
1984
|
+
clientLoader;
|
|
1985
|
+
lastInitFailure = 0;
|
|
1986
|
+
systemPrompt = "";
|
|
1987
|
+
roomSessions = /* @__PURE__ */ new Map();
|
|
1988
|
+
roomCustomers = /* @__PURE__ */ new Map();
|
|
1989
|
+
bootstrappedRooms = /* @__PURE__ */ new Set();
|
|
1990
|
+
roomSessionInitPromises = /* @__PURE__ */ new Map();
|
|
1991
|
+
roomBootstrapInitPromises = /* @__PURE__ */ new Map();
|
|
1992
|
+
constructor(options) {
|
|
1993
|
+
super({
|
|
1994
|
+
historyConverter: options.historyConverter ?? new ParlantHistoryConverter()
|
|
1995
|
+
});
|
|
1996
|
+
this.environment = options.environment;
|
|
1997
|
+
this.baseUrl = options.baseUrl;
|
|
1998
|
+
this.agentId = options.agentId;
|
|
1999
|
+
this.apiKey = options.apiKey;
|
|
2000
|
+
this.headers = { ...options.headers ?? {} };
|
|
2001
|
+
this.systemPromptOverride = options.systemPrompt;
|
|
2002
|
+
this.customSection = options.customSection;
|
|
2003
|
+
this.includeBaseInstructions = options.includeBaseInstructions ?? true;
|
|
2004
|
+
this.responseTimeoutSeconds = options.responseTimeoutSeconds ?? DEFAULT_RESPONSE_TIMEOUT_SECONDS;
|
|
2005
|
+
this.maxHistoryMessages = options.maxHistoryMessages ?? DEFAULT_MAX_HISTORY_MESSAGES;
|
|
2006
|
+
this.clientFactory = options.clientFactory;
|
|
2007
|
+
this.logger = options.logger ?? new NoopLogger();
|
|
2008
|
+
this.clientLoader = new LazyAsyncValue({
|
|
2009
|
+
load: async () => this.createClient(),
|
|
2010
|
+
onRejected: (error) => {
|
|
2011
|
+
this.lastInitFailure = Date.now();
|
|
2012
|
+
this.logger.error("Parlant client initialization failed", {
|
|
2013
|
+
error
|
|
2014
|
+
});
|
|
2015
|
+
}
|
|
2016
|
+
});
|
|
2017
|
+
}
|
|
2018
|
+
async onStarted(agentName, agentDescription) {
|
|
2019
|
+
await super.onStarted(agentName, agentDescription);
|
|
2020
|
+
this.systemPrompt = this.systemPromptOverride ?? renderSystemPrompt({
|
|
2021
|
+
agentName,
|
|
2022
|
+
agentDescription,
|
|
2023
|
+
customSection: this.customSection,
|
|
2024
|
+
includeBaseInstructions: this.includeBaseInstructions
|
|
2025
|
+
});
|
|
2026
|
+
}
|
|
2027
|
+
async onMessage(message, tools, history, participantsMessage, contactsMessage, context) {
|
|
2028
|
+
const senderName = message.senderName ?? message.senderId ?? "User";
|
|
2029
|
+
try {
|
|
2030
|
+
const client = await this.ensureClient();
|
|
2031
|
+
const sessionId = await this.getOrCreateSession(
|
|
2032
|
+
client,
|
|
2033
|
+
context.roomId,
|
|
2034
|
+
senderName
|
|
2035
|
+
);
|
|
2036
|
+
if (context.isSessionBootstrap) {
|
|
2037
|
+
await this.ensureBootstrapHistory(client, sessionId, context.roomId, history);
|
|
2038
|
+
}
|
|
2039
|
+
const userMessage = buildUserMessage({
|
|
2040
|
+
content: message.content,
|
|
2041
|
+
participantsMessage,
|
|
2042
|
+
contactsMessage
|
|
2043
|
+
});
|
|
2044
|
+
const createdEvent = await client.sessions.createEvent(
|
|
2045
|
+
sessionId,
|
|
2046
|
+
{
|
|
2047
|
+
kind: "message",
|
|
2048
|
+
source: "customer",
|
|
2049
|
+
message: userMessage,
|
|
2050
|
+
moderation: "none",
|
|
2051
|
+
metadata: {
|
|
2052
|
+
thenvoi_source: "thenvoi-sdk-typescript",
|
|
2053
|
+
thenvoi_room_id: context.roomId
|
|
2054
|
+
}
|
|
2055
|
+
},
|
|
2056
|
+
this.requestOptions()
|
|
2057
|
+
);
|
|
2058
|
+
const reply = await this.waitForAiResponse(
|
|
2059
|
+
client,
|
|
2060
|
+
sessionId,
|
|
2061
|
+
createdEvent.offset
|
|
2062
|
+
);
|
|
2063
|
+
if (!reply) {
|
|
2064
|
+
await tools.sendEvent(
|
|
2065
|
+
"Parlant did not return a response before timeout.",
|
|
2066
|
+
"error",
|
|
2067
|
+
{
|
|
2068
|
+
parlant_session_id: sessionId,
|
|
2069
|
+
parlant_timeout_seconds: this.responseTimeoutSeconds
|
|
2070
|
+
}
|
|
2071
|
+
);
|
|
2072
|
+
return;
|
|
2073
|
+
}
|
|
2074
|
+
await tools.sendMessage(reply, [{ id: message.senderId }]);
|
|
2075
|
+
} catch (error) {
|
|
2076
|
+
const errorMessage = asErrorMessage(error);
|
|
2077
|
+
this.logger.error("Parlant adapter request failed", {
|
|
2078
|
+
roomId: context.roomId,
|
|
2079
|
+
agentId: this.agentId,
|
|
2080
|
+
error
|
|
2081
|
+
});
|
|
2082
|
+
try {
|
|
2083
|
+
await tools.sendEvent(
|
|
2084
|
+
`Parlant adapter error: ${errorMessage}`,
|
|
2085
|
+
"error",
|
|
2086
|
+
{
|
|
2087
|
+
parlant_error: errorMessage
|
|
2088
|
+
}
|
|
2089
|
+
);
|
|
2090
|
+
} catch (eventError) {
|
|
2091
|
+
this.logger.warn("Parlant adapter failed to emit error event", {
|
|
2092
|
+
roomId: context.roomId,
|
|
2093
|
+
agentId: this.agentId,
|
|
2094
|
+
error: eventError
|
|
2095
|
+
});
|
|
2096
|
+
}
|
|
2097
|
+
throw error instanceof Error ? error : new Error(errorMessage);
|
|
2098
|
+
}
|
|
2099
|
+
}
|
|
2100
|
+
async onCleanup(roomId) {
|
|
2101
|
+
const pendingSession = this.roomSessionInitPromises.get(roomId);
|
|
2102
|
+
const pendingBootstrap = this.roomBootstrapInitPromises.get(roomId);
|
|
2103
|
+
const pendingOperations = [pendingSession, pendingBootstrap].filter(
|
|
2104
|
+
(promise) => promise !== void 0
|
|
2105
|
+
);
|
|
2106
|
+
await Promise.allSettled(pendingOperations);
|
|
2107
|
+
this.roomSessions.delete(roomId);
|
|
2108
|
+
this.roomCustomers.delete(roomId);
|
|
2109
|
+
this.bootstrappedRooms.delete(roomId);
|
|
2110
|
+
this.roomSessionInitPromises.delete(roomId);
|
|
2111
|
+
this.roomBootstrapInitPromises.delete(roomId);
|
|
2112
|
+
}
|
|
2113
|
+
async getOrCreateSession(client, roomId, customerName) {
|
|
2114
|
+
const existingSession = this.roomSessions.get(roomId);
|
|
2115
|
+
if (existingSession) {
|
|
2116
|
+
return existingSession;
|
|
2117
|
+
}
|
|
2118
|
+
const initializing = this.roomSessionInitPromises.get(roomId);
|
|
2119
|
+
if (initializing) {
|
|
2120
|
+
return initializing;
|
|
2121
|
+
}
|
|
2122
|
+
const initPromise = (async () => {
|
|
2123
|
+
const customerId = await this.getOrCreateCustomer(client, roomId, customerName);
|
|
2124
|
+
const session = await client.sessions.create(
|
|
2125
|
+
{
|
|
2126
|
+
agentId: this.agentId,
|
|
2127
|
+
customerId,
|
|
2128
|
+
title: `Thenvoi Room ${roomId.slice(0, 8)}`,
|
|
2129
|
+
metadata: {
|
|
2130
|
+
thenvoi_room_id: roomId
|
|
2131
|
+
}
|
|
2132
|
+
},
|
|
2133
|
+
this.requestOptions()
|
|
2134
|
+
);
|
|
2135
|
+
if (this.systemPrompt.trim().length > 0) {
|
|
2136
|
+
await client.sessions.createEvent(
|
|
2137
|
+
session.id,
|
|
2138
|
+
{
|
|
2139
|
+
kind: "message",
|
|
2140
|
+
source: "system",
|
|
2141
|
+
message: this.systemPrompt,
|
|
2142
|
+
metadata: {
|
|
2143
|
+
thenvoi_system_prompt: true
|
|
2144
|
+
}
|
|
2145
|
+
},
|
|
2146
|
+
this.requestOptions()
|
|
2147
|
+
);
|
|
2148
|
+
}
|
|
2149
|
+
this.roomSessions.set(roomId, session.id);
|
|
2150
|
+
return session.id;
|
|
2151
|
+
})();
|
|
2152
|
+
this.roomSessionInitPromises.set(roomId, initPromise);
|
|
2153
|
+
try {
|
|
2154
|
+
return await initPromise;
|
|
2155
|
+
} finally {
|
|
2156
|
+
const pending = this.roomSessionInitPromises.get(roomId);
|
|
2157
|
+
if (pending === initPromise) {
|
|
2158
|
+
this.roomSessionInitPromises.delete(roomId);
|
|
2159
|
+
}
|
|
2160
|
+
}
|
|
2161
|
+
}
|
|
2162
|
+
async ensureBootstrapHistory(client, sessionId, roomId, history) {
|
|
2163
|
+
if (this.bootstrappedRooms.has(roomId)) {
|
|
2164
|
+
return;
|
|
2165
|
+
}
|
|
2166
|
+
const initializing = this.roomBootstrapInitPromises.get(roomId);
|
|
2167
|
+
if (initializing) {
|
|
2168
|
+
await initializing;
|
|
2169
|
+
return;
|
|
2170
|
+
}
|
|
2171
|
+
const initPromise = (async () => {
|
|
2172
|
+
if (this.bootstrappedRooms.has(roomId)) {
|
|
2173
|
+
return;
|
|
2174
|
+
}
|
|
2175
|
+
await this.injectHistory(client, sessionId, history);
|
|
2176
|
+
this.bootstrappedRooms.add(roomId);
|
|
2177
|
+
})();
|
|
2178
|
+
this.roomBootstrapInitPromises.set(roomId, initPromise);
|
|
2179
|
+
try {
|
|
2180
|
+
await initPromise;
|
|
2181
|
+
} finally {
|
|
2182
|
+
const pending = this.roomBootstrapInitPromises.get(roomId);
|
|
2183
|
+
if (pending === initPromise) {
|
|
2184
|
+
this.roomBootstrapInitPromises.delete(roomId);
|
|
2185
|
+
}
|
|
2186
|
+
}
|
|
2187
|
+
}
|
|
2188
|
+
async getOrCreateCustomer(client, roomId, customerName) {
|
|
2189
|
+
const existingCustomer = this.roomCustomers.get(roomId);
|
|
2190
|
+
if (existingCustomer) {
|
|
2191
|
+
return existingCustomer;
|
|
2192
|
+
}
|
|
2193
|
+
const customer = await client.customers.create(
|
|
2194
|
+
{
|
|
2195
|
+
name: customerName,
|
|
2196
|
+
metadata: {
|
|
2197
|
+
thenvoi_room_id: roomId
|
|
2198
|
+
}
|
|
2199
|
+
},
|
|
2200
|
+
this.requestOptions()
|
|
2201
|
+
);
|
|
2202
|
+
this.roomCustomers.set(roomId, customer.id);
|
|
2203
|
+
return customer.id;
|
|
2204
|
+
}
|
|
2205
|
+
async injectHistory(client, sessionId, history) {
|
|
2206
|
+
if (history.length === 0) {
|
|
2207
|
+
return;
|
|
2208
|
+
}
|
|
2209
|
+
const completeHistory = selectCompleteExchanges(history).slice(
|
|
2210
|
+
-this.maxHistoryMessages
|
|
2211
|
+
);
|
|
2212
|
+
let failedEvents = 0;
|
|
2213
|
+
for (const item of completeHistory) {
|
|
2214
|
+
try {
|
|
2215
|
+
if (item.role === "user") {
|
|
2216
|
+
await client.sessions.createEvent(
|
|
2217
|
+
sessionId,
|
|
2218
|
+
{
|
|
2219
|
+
kind: "message",
|
|
2220
|
+
source: "customer",
|
|
2221
|
+
message: item.content,
|
|
2222
|
+
moderation: "none",
|
|
2223
|
+
metadata: {
|
|
2224
|
+
historical: true
|
|
2225
|
+
}
|
|
2226
|
+
},
|
|
2227
|
+
this.requestOptions()
|
|
2228
|
+
);
|
|
2229
|
+
continue;
|
|
2230
|
+
}
|
|
2231
|
+
await client.sessions.createEvent(
|
|
2232
|
+
sessionId,
|
|
2233
|
+
{
|
|
2234
|
+
kind: "message",
|
|
2235
|
+
source: "ai_agent",
|
|
2236
|
+
data: {
|
|
2237
|
+
message: item.content,
|
|
2238
|
+
participant: {
|
|
2239
|
+
displayName: item.sender || this.agentName || "Assistant"
|
|
2240
|
+
}
|
|
2241
|
+
},
|
|
2242
|
+
metadata: {
|
|
2243
|
+
historical: true
|
|
2244
|
+
}
|
|
2245
|
+
},
|
|
2246
|
+
this.requestOptions()
|
|
2247
|
+
);
|
|
2248
|
+
} catch (error) {
|
|
2249
|
+
failedEvents += 1;
|
|
2250
|
+
this.logger.warn("Parlant history injection failed", {
|
|
2251
|
+
sessionId,
|
|
2252
|
+
roomRole: item.role,
|
|
2253
|
+
error
|
|
2254
|
+
});
|
|
2255
|
+
}
|
|
2256
|
+
}
|
|
2257
|
+
if (failedEvents > 0) {
|
|
2258
|
+
this.logger.warn("Parlant history injection completed with skipped events", {
|
|
2259
|
+
sessionId,
|
|
2260
|
+
failedEvents
|
|
2261
|
+
});
|
|
2262
|
+
}
|
|
2263
|
+
}
|
|
2264
|
+
async waitForAiResponse(client, sessionId, minOffset) {
|
|
2265
|
+
const deadline = Date.now() + this.responseTimeoutSeconds * 1e3;
|
|
2266
|
+
let nextOffset = Math.max(0, minOffset + 1);
|
|
2267
|
+
while (Date.now() < deadline) {
|
|
2268
|
+
const remainingSeconds = Math.max(
|
|
2269
|
+
1,
|
|
2270
|
+
Math.ceil((deadline - Date.now()) / 1e3)
|
|
2271
|
+
);
|
|
2272
|
+
const waitForData = Math.min(10, remainingSeconds);
|
|
2273
|
+
const events = await client.sessions.listEvents(
|
|
2274
|
+
sessionId,
|
|
2275
|
+
{
|
|
2276
|
+
minOffset: nextOffset,
|
|
2277
|
+
source: "ai_agent",
|
|
2278
|
+
kinds: "message,status",
|
|
2279
|
+
waitForData
|
|
2280
|
+
},
|
|
2281
|
+
this.requestOptions()
|
|
2282
|
+
);
|
|
2283
|
+
if (!Array.isArray(events) || events.length === 0) {
|
|
2284
|
+
continue;
|
|
2285
|
+
}
|
|
2286
|
+
const ordered = [...events].sort((left, right) => {
|
|
2287
|
+
const leftOffset = asNumber(left.offset) ?? Number.MAX_SAFE_INTEGER;
|
|
2288
|
+
const rightOffset = asNumber(right.offset) ?? Number.MAX_SAFE_INTEGER;
|
|
2289
|
+
return leftOffset - rightOffset;
|
|
2290
|
+
});
|
|
2291
|
+
for (const event of ordered) {
|
|
2292
|
+
const offset = asNumber(event.offset);
|
|
2293
|
+
if (offset !== null) {
|
|
2294
|
+
nextOffset = Math.max(nextOffset, offset + 1);
|
|
2295
|
+
}
|
|
2296
|
+
const state = extractStatusState(event);
|
|
2297
|
+
if (state === "error" || state === "cancelled") {
|
|
2298
|
+
throw new Error(
|
|
2299
|
+
`Parlant session ${sessionId} entered terminal status: ${state}`
|
|
2300
|
+
);
|
|
2301
|
+
}
|
|
2302
|
+
const text = extractEventMessage(event);
|
|
2303
|
+
if (text) {
|
|
2304
|
+
return text;
|
|
2305
|
+
}
|
|
2306
|
+
}
|
|
2307
|
+
}
|
|
2308
|
+
return null;
|
|
2309
|
+
}
|
|
2310
|
+
requestOptions() {
|
|
2311
|
+
const headers = { ...this.headers };
|
|
2312
|
+
if (this.apiKey) {
|
|
2313
|
+
headers.Authorization = `Bearer ${this.apiKey}`;
|
|
2314
|
+
}
|
|
2315
|
+
if (Object.keys(headers).length === 0) {
|
|
2316
|
+
return void 0;
|
|
2317
|
+
}
|
|
2318
|
+
return { headers };
|
|
2319
|
+
}
|
|
2320
|
+
async ensureClient() {
|
|
2321
|
+
if (this.clientLoader.current) {
|
|
2322
|
+
return this.clientLoader.get();
|
|
2323
|
+
}
|
|
2324
|
+
const cooldownMs = 2e3;
|
|
2325
|
+
const elapsed = Date.now() - this.lastInitFailure;
|
|
2326
|
+
if (this.lastInitFailure > 0 && elapsed < cooldownMs) {
|
|
2327
|
+
throw new Error(
|
|
2328
|
+
`Parlant client init failed recently (${elapsed}ms ago). Retrying after ${cooldownMs}ms cooldown.`
|
|
2329
|
+
);
|
|
2330
|
+
}
|
|
2331
|
+
return this.clientLoader.get();
|
|
2332
|
+
}
|
|
2333
|
+
async createClient() {
|
|
2334
|
+
const factory = this.clientFactory ?? await loadParlantClientFactory({
|
|
2335
|
+
environment: this.environment,
|
|
2336
|
+
baseUrl: this.baseUrl
|
|
2337
|
+
});
|
|
2338
|
+
return factory();
|
|
2339
|
+
}
|
|
2340
|
+
};
|
|
2341
|
+
function selectCompleteExchanges(history) {
|
|
2342
|
+
const complete = [];
|
|
2343
|
+
let index = 0;
|
|
2344
|
+
while (index < history.length) {
|
|
2345
|
+
const current = history[index];
|
|
2346
|
+
if (current.role === "user" && current.content) {
|
|
2347
|
+
const next = history[index + 1];
|
|
2348
|
+
if (next && next.role === "assistant" && next.content) {
|
|
2349
|
+
complete.push(current);
|
|
2350
|
+
complete.push(next);
|
|
2351
|
+
index += 2;
|
|
2352
|
+
continue;
|
|
2353
|
+
}
|
|
2354
|
+
index += 1;
|
|
2355
|
+
continue;
|
|
2356
|
+
}
|
|
2357
|
+
index += 1;
|
|
2358
|
+
}
|
|
2359
|
+
return complete;
|
|
2360
|
+
}
|
|
2361
|
+
function buildUserMessage(input) {
|
|
2362
|
+
const updates = [];
|
|
2363
|
+
if (input.participantsMessage) {
|
|
2364
|
+
updates.push(`[System Update]: ${input.participantsMessage}`);
|
|
2365
|
+
}
|
|
2366
|
+
if (input.contactsMessage) {
|
|
2367
|
+
updates.push(`[System Update]: ${input.contactsMessage}`);
|
|
2368
|
+
}
|
|
2369
|
+
if (updates.length === 0) {
|
|
2370
|
+
return input.content;
|
|
2371
|
+
}
|
|
2372
|
+
return `${updates.join("\n\n")}
|
|
2373
|
+
|
|
2374
|
+
${input.content}`;
|
|
2375
|
+
}
|
|
2376
|
+
async function loadParlantClientFactory(config) {
|
|
2377
|
+
const module = await import("parlant-client").catch((error) => {
|
|
2378
|
+
throw new UnsupportedFeatureError(
|
|
2379
|
+
`ParlantAdapter requires optional dependency parlant-client. Install it with "pnpm add parlant-client". (${error instanceof Error ? error.message : String(error)})`
|
|
2380
|
+
);
|
|
2381
|
+
});
|
|
2382
|
+
if (!module.ParlantClient) {
|
|
2383
|
+
throw new UnsupportedFeatureError(
|
|
2384
|
+
'ParlantAdapter requires optional dependency parlant-client. Install it with "pnpm add parlant-client".'
|
|
2385
|
+
);
|
|
2386
|
+
}
|
|
2387
|
+
const ParlantClientCtor = module.ParlantClient;
|
|
2388
|
+
return async () => new ParlantClientCtor({
|
|
2389
|
+
environment: () => config.environment,
|
|
2390
|
+
...config.baseUrl ? { baseUrl: () => config.baseUrl } : {}
|
|
2391
|
+
});
|
|
2392
|
+
}
|
|
2393
|
+
function extractStatusState(event) {
|
|
2394
|
+
if ((asNonEmptyString(event.kind) ?? "") !== "status") {
|
|
2395
|
+
return null;
|
|
2396
|
+
}
|
|
2397
|
+
const data = asOptionalRecord(event.data) ?? {};
|
|
2398
|
+
return asNonEmptyString(data.state) ?? null;
|
|
2399
|
+
}
|
|
2400
|
+
function extractEventMessage(event) {
|
|
2401
|
+
const directData = event.data;
|
|
2402
|
+
if (typeof directData === "string" && directData.trim().length > 0) {
|
|
2403
|
+
return directData.trim();
|
|
2404
|
+
}
|
|
2405
|
+
const data = asOptionalRecord(directData) ?? {};
|
|
2406
|
+
const message = data.message;
|
|
2407
|
+
if (typeof message === "string" && message.trim().length > 0) {
|
|
2408
|
+
return message.trim();
|
|
2409
|
+
}
|
|
2410
|
+
if (message && typeof message === "object") {
|
|
2411
|
+
const messageObject = message;
|
|
2412
|
+
const text = asNonEmptyString(messageObject.text) ?? asNonEmptyString(messageObject.content);
|
|
2413
|
+
if (text) {
|
|
2414
|
+
return text;
|
|
2415
|
+
}
|
|
2416
|
+
}
|
|
2417
|
+
const content = asNonEmptyString(data.content);
|
|
2418
|
+
if (content) {
|
|
2419
|
+
return content;
|
|
2420
|
+
}
|
|
2421
|
+
const chunks = data.chunks;
|
|
2422
|
+
if (Array.isArray(chunks)) {
|
|
2423
|
+
const lines = chunks.filter((chunk) => typeof chunk === "string").map((chunk) => chunk.trim()).filter((chunk) => chunk.length > 0);
|
|
2424
|
+
if (lines.length > 0) {
|
|
2425
|
+
return lines.join("");
|
|
2426
|
+
}
|
|
2427
|
+
}
|
|
2428
|
+
return null;
|
|
2429
|
+
}
|
|
2430
|
+
function asNumber(value) {
|
|
2431
|
+
if (typeof value === "number" && Number.isFinite(value)) {
|
|
2432
|
+
return value;
|
|
2433
|
+
}
|
|
2434
|
+
return null;
|
|
2435
|
+
}
|
|
2436
|
+
|
|
2437
|
+
// src/converters/opencode.ts
|
|
2438
|
+
var OpencodeHistoryConverter = class {
|
|
2439
|
+
setAgentName(_name) {
|
|
2440
|
+
}
|
|
2441
|
+
convert(raw) {
|
|
2442
|
+
const replayMessages = buildReplayMessages(raw);
|
|
2443
|
+
const metadata = findLatestTaskMetadata(
|
|
2444
|
+
raw,
|
|
2445
|
+
(entry) => typeof entry.opencode_session_id === "string" && entry.opencode_session_id.length > 0
|
|
2446
|
+
);
|
|
2447
|
+
if (!metadata) {
|
|
2448
|
+
return {
|
|
2449
|
+
sessionId: null,
|
|
2450
|
+
roomId: null,
|
|
2451
|
+
createdAt: null,
|
|
2452
|
+
replayMessages
|
|
2453
|
+
};
|
|
2454
|
+
}
|
|
2455
|
+
return {
|
|
2456
|
+
sessionId: asOptionalString(metadata.opencode_session_id),
|
|
2457
|
+
roomId: asOptionalString(metadata.opencode_room_id),
|
|
2458
|
+
createdAt: parseDate(metadata.opencode_created_at),
|
|
2459
|
+
replayMessages
|
|
2460
|
+
};
|
|
2461
|
+
}
|
|
2462
|
+
};
|
|
2463
|
+
function extractOpencodeSessionId(raw) {
|
|
2464
|
+
return asOptionalString(
|
|
2465
|
+
findLatestTaskMetadata(
|
|
2466
|
+
raw,
|
|
2467
|
+
(entry) => typeof entry.opencode_session_id === "string" && entry.opencode_session_id.length > 0
|
|
2468
|
+
)?.opencode_session_id
|
|
2469
|
+
);
|
|
2470
|
+
}
|
|
2471
|
+
function buildReplayMessages(raw) {
|
|
2472
|
+
const replayMessages = [];
|
|
2473
|
+
for (const entry of raw) {
|
|
2474
|
+
if (String(entry.message_type ?? "text") !== "text") {
|
|
2475
|
+
continue;
|
|
2476
|
+
}
|
|
2477
|
+
const content = asOptionalString(entry.content);
|
|
2478
|
+
if (!content) {
|
|
2479
|
+
continue;
|
|
2480
|
+
}
|
|
2481
|
+
const senderName = asOptionalString(entry.sender_name) ?? asOptionalString(entry.sender_type) ?? "Unknown";
|
|
2482
|
+
replayMessages.push(`[${senderName}]: ${content}`);
|
|
2483
|
+
}
|
|
2484
|
+
return replayMessages;
|
|
2485
|
+
}
|
|
2486
|
+
|
|
2487
|
+
// src/converters/claude-sdk.ts
|
|
2488
|
+
var ClaudeSDKHistoryConverter = class {
|
|
2489
|
+
agentName = "";
|
|
2490
|
+
constructor(agentName = "") {
|
|
2491
|
+
this.agentName = agentName;
|
|
2492
|
+
}
|
|
2493
|
+
setAgentName(name) {
|
|
2494
|
+
this.agentName = name;
|
|
2495
|
+
}
|
|
2496
|
+
convert(raw) {
|
|
2497
|
+
return {
|
|
2498
|
+
text: buildClaudeSdkText(raw, this.agentName),
|
|
2499
|
+
sessionId: extractClaudeSessionId(raw)
|
|
2500
|
+
};
|
|
2501
|
+
}
|
|
2502
|
+
};
|
|
2503
|
+
function extractClaudeSessionId(raw) {
|
|
2504
|
+
const metadata = findLatestTaskMetadata(
|
|
2505
|
+
raw,
|
|
2506
|
+
(entry) => typeof entry.claude_sdk_session_id === "string" && entry.claude_sdk_session_id.length > 0 || typeof entry.claude_session_id === "string" && entry.claude_session_id.length > 0
|
|
2507
|
+
);
|
|
2508
|
+
const sessionId = metadata?.claude_sdk_session_id ?? metadata?.claude_session_id;
|
|
2509
|
+
return typeof sessionId === "string" && sessionId.length > 0 ? sessionId : null;
|
|
2510
|
+
}
|
|
2511
|
+
function buildClaudeSdkText(raw, agentName) {
|
|
2512
|
+
const lines = [];
|
|
2513
|
+
for (const entry of raw) {
|
|
2514
|
+
const messageType = String(entry.message_type ?? "text");
|
|
2515
|
+
if (messageType === "task") {
|
|
2516
|
+
continue;
|
|
2517
|
+
}
|
|
2518
|
+
const content = asOptionalString(entry.content);
|
|
2519
|
+
if (!content) {
|
|
2520
|
+
continue;
|
|
2521
|
+
}
|
|
2522
|
+
const role = String(entry.role ?? "user");
|
|
2523
|
+
const senderName = asOptionalString(entry.sender_name) ?? "Unknown";
|
|
2524
|
+
if (messageType === "text") {
|
|
2525
|
+
if (role === "assistant" && senderName === agentName) {
|
|
2526
|
+
continue;
|
|
2527
|
+
}
|
|
2528
|
+
lines.push(`[${senderName}]: ${content}`);
|
|
2529
|
+
continue;
|
|
2530
|
+
}
|
|
2531
|
+
if (messageType === "tool_call" || messageType === "tool_result") {
|
|
2532
|
+
lines.push(content);
|
|
2533
|
+
}
|
|
2534
|
+
}
|
|
2535
|
+
return lines.join("\n");
|
|
2536
|
+
}
|
|
2537
|
+
|
|
2538
|
+
export {
|
|
2539
|
+
ACPClientHistoryConverter,
|
|
2540
|
+
ACPServerHistoryConverter,
|
|
2541
|
+
asOptionalString,
|
|
2542
|
+
parseDate,
|
|
2543
|
+
parseToolCall,
|
|
2544
|
+
parseToolResult,
|
|
2545
|
+
LazyAsyncValue,
|
|
2546
|
+
findLatestTaskMetadata,
|
|
2547
|
+
GoogleADKHistoryConverter,
|
|
2548
|
+
extractGoogleAdkSessionId,
|
|
2549
|
+
buildA2AAuthHeaders,
|
|
2550
|
+
A2AHistoryConverter,
|
|
2551
|
+
A2AAdapter,
|
|
2552
|
+
GatewayHistoryConverter,
|
|
2553
|
+
GatewayServer,
|
|
2554
|
+
createGatewayServer,
|
|
2555
|
+
A2AGatewayAdapter,
|
|
2556
|
+
ParlantHistoryConverter,
|
|
2557
|
+
ParlantAdapter,
|
|
2558
|
+
OpencodeHistoryConverter,
|
|
2559
|
+
extractOpencodeSessionId,
|
|
2560
|
+
ClaudeSDKHistoryConverter,
|
|
2561
|
+
extractClaudeSessionId
|
|
2562
|
+
};
|