@huydao/karrot 0.1.1
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/GUIDE.md +484 -0
- package/README.md +253 -0
- package/dist/assertions/assertion.d.ts +18 -0
- package/dist/assertions/assertion.js +198 -0
- package/dist/assertions/turn-eval.d.ts +22 -0
- package/dist/assertions/turn-eval.js +178 -0
- package/dist/executors/adapters/ag-ui-post.d.ts +55 -0
- package/dist/executors/adapters/ag-ui-post.js +703 -0
- package/dist/executors/adapters/ag-ui.d.ts +15 -0
- package/dist/executors/adapters/ag-ui.js +275 -0
- package/dist/executors/execute.d.ts +16 -0
- package/dist/executors/execute.js +145 -0
- package/dist/executors/executor.d.ts +37 -0
- package/dist/executors/executor.js +203 -0
- package/dist/executors/run-result.d.ts +33 -0
- package/dist/executors/run-result.js +22 -0
- package/dist/index.d.ts +12 -0
- package/dist/index.js +28 -0
- package/dist/prompts/turn-eval-system-prompt.md +68 -0
- package/dist/prompts/turn-message-gen-system-prompt.md +16 -0
- package/dist/reports/report.d.ts +68 -0
- package/dist/reports/report.js +366 -0
- package/dist/scenarios/generated-message.d.ts +15 -0
- package/dist/scenarios/generated-message.js +116 -0
- package/dist/scenarios/scenario-loader.d.ts +12 -0
- package/dist/scenarios/scenario-loader.js +103 -0
- package/dist/scenarios/scenario.d.ts +62 -0
- package/dist/scenarios/scenario.js +35 -0
- package/dist/utils/artifact-files.d.ts +3 -0
- package/dist/utils/artifact-files.js +22 -0
- package/dist/utils/config.d.ts +101 -0
- package/dist/utils/config.js +57 -0
- package/dist/utils/openai-eval.d.ts +5 -0
- package/dist/utils/openai-eval.js +54 -0
- package/package.json +146 -0
|
@@ -0,0 +1,703 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.runAgUiPostMessage = runAgUiPostMessage;
|
|
7
|
+
const node_crypto_1 = __importDefault(require("node:crypto"));
|
|
8
|
+
const promises_1 = __importDefault(require("node:fs/promises"));
|
|
9
|
+
const node_path_1 = __importDefault(require("node:path"));
|
|
10
|
+
const run_result_1 = require("../run-result");
|
|
11
|
+
function getValueByPath(payload, pathExpression) {
|
|
12
|
+
return pathExpression
|
|
13
|
+
.split('.')
|
|
14
|
+
.filter(Boolean)
|
|
15
|
+
.reduce((currentValue, segment) => {
|
|
16
|
+
if (!currentValue || typeof currentValue !== 'object') {
|
|
17
|
+
return undefined;
|
|
18
|
+
}
|
|
19
|
+
return currentValue[segment];
|
|
20
|
+
}, payload);
|
|
21
|
+
}
|
|
22
|
+
async function waitForCompletionCheck(options) {
|
|
23
|
+
const deadline = Date.now() + (options.timeoutMs ?? 300_000);
|
|
24
|
+
const intervalMs = options.intervalMs ?? 5_000;
|
|
25
|
+
while (Date.now() < deadline) {
|
|
26
|
+
const response = await fetch(options.url, {
|
|
27
|
+
method: 'GET',
|
|
28
|
+
headers: {
|
|
29
|
+
accept: 'application/json',
|
|
30
|
+
...(options.headers ?? {}),
|
|
31
|
+
},
|
|
32
|
+
});
|
|
33
|
+
if (!response.ok) {
|
|
34
|
+
throw new Error(`Completion check failed with HTTP ${response.status}.`);
|
|
35
|
+
}
|
|
36
|
+
const payload = await response.json();
|
|
37
|
+
const rawStatus = getValueByPath(payload, options.statusPath);
|
|
38
|
+
const status = typeof rawStatus === 'string' ? rawStatus.trim() : '';
|
|
39
|
+
if (!status) {
|
|
40
|
+
throw new Error(`Completion check did not find a status at "${options.statusPath}".`);
|
|
41
|
+
}
|
|
42
|
+
if (options.successStatuses.includes(status)) {
|
|
43
|
+
return {
|
|
44
|
+
status,
|
|
45
|
+
payload,
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
if (options.failureStatuses?.includes(status)) {
|
|
49
|
+
throw new Error(`Completion check reached failure status "${status}".`);
|
|
50
|
+
}
|
|
51
|
+
await new Promise((resolve) => setTimeout(resolve, intervalMs));
|
|
52
|
+
}
|
|
53
|
+
throw new Error(`Completion check timed out after ${options.timeoutMs ?? 300_000}ms.`);
|
|
54
|
+
}
|
|
55
|
+
function getStringAtPath(payload, pathExpression) {
|
|
56
|
+
const rawValue = getValueByPath(payload, pathExpression);
|
|
57
|
+
return typeof rawValue === 'string' ? rawValue.trim() : '';
|
|
58
|
+
}
|
|
59
|
+
function getArrayAtPath(payload, pathExpression) {
|
|
60
|
+
const rawValue = getValueByPath(payload, pathExpression);
|
|
61
|
+
return Array.isArray(rawValue) ? rawValue : [];
|
|
62
|
+
}
|
|
63
|
+
async function appendObserveEvent(outputPath, event) {
|
|
64
|
+
await promises_1.default.appendFile(outputPath, `${JSON.stringify(event)}\n`, 'utf8');
|
|
65
|
+
}
|
|
66
|
+
async function waitForObservedCompletion(options) {
|
|
67
|
+
const seenProgressIds = new Set();
|
|
68
|
+
const deadline = Date.now() + (options.observe.timeoutMs ?? 300_000);
|
|
69
|
+
const intervalMs = options.observe.intervalMs ?? 5_000;
|
|
70
|
+
let latestOutput = options.fallbackOutput;
|
|
71
|
+
while (Date.now() < deadline) {
|
|
72
|
+
for (const endpoint of options.observe.progressEndpoints ?? []) {
|
|
73
|
+
const response = await fetch(endpoint.url, {
|
|
74
|
+
method: 'GET',
|
|
75
|
+
headers: {
|
|
76
|
+
accept: 'application/json',
|
|
77
|
+
...(endpoint.headers ?? {}),
|
|
78
|
+
},
|
|
79
|
+
});
|
|
80
|
+
if (response.status === 404) {
|
|
81
|
+
continue;
|
|
82
|
+
}
|
|
83
|
+
if (!response.ok) {
|
|
84
|
+
throw new Error(`Observe progress request failed with HTTP ${response.status}.`);
|
|
85
|
+
}
|
|
86
|
+
const payload = await response.json();
|
|
87
|
+
const items = getArrayAtPath(payload, endpoint.itemsPath);
|
|
88
|
+
for (const item of items) {
|
|
89
|
+
const itemId = endpoint.idPath && getStringAtPath(item, endpoint.idPath)
|
|
90
|
+
? getStringAtPath(item, endpoint.idPath)
|
|
91
|
+
: JSON.stringify(item);
|
|
92
|
+
if (seenProgressIds.has(itemId)) {
|
|
93
|
+
continue;
|
|
94
|
+
}
|
|
95
|
+
seenProgressIds.add(itemId);
|
|
96
|
+
await appendObserveEvent(options.outputPath, {
|
|
97
|
+
source: 'observe-progress',
|
|
98
|
+
type: endpoint.eventType ?? 'progress',
|
|
99
|
+
id: itemId,
|
|
100
|
+
item,
|
|
101
|
+
observedAt: new Date().toISOString(),
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
if (options.observe.outputEndpoint) {
|
|
106
|
+
const response = await fetch(options.observe.outputEndpoint.url, {
|
|
107
|
+
method: 'GET',
|
|
108
|
+
headers: {
|
|
109
|
+
accept: 'application/json',
|
|
110
|
+
...(options.observe.outputEndpoint.headers ?? {}),
|
|
111
|
+
},
|
|
112
|
+
});
|
|
113
|
+
if (response.status !== 404) {
|
|
114
|
+
if (!response.ok) {
|
|
115
|
+
throw new Error(`Observe output request failed with HTTP ${response.status}.`);
|
|
116
|
+
}
|
|
117
|
+
const payload = await response.json();
|
|
118
|
+
const observedOutput = getStringAtPath(payload, options.observe.outputEndpoint.textPath);
|
|
119
|
+
if (observedOutput && observedOutput !== latestOutput) {
|
|
120
|
+
latestOutput = observedOutput;
|
|
121
|
+
await appendObserveEvent(options.outputPath, {
|
|
122
|
+
source: 'observe-output',
|
|
123
|
+
type: 'output',
|
|
124
|
+
outputLength: observedOutput.length,
|
|
125
|
+
observedAt: new Date().toISOString(),
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
const statusResponse = await fetch(options.observe.status.url, {
|
|
131
|
+
method: 'GET',
|
|
132
|
+
headers: {
|
|
133
|
+
accept: 'application/json',
|
|
134
|
+
...(options.observe.status.headers ?? {}),
|
|
135
|
+
},
|
|
136
|
+
});
|
|
137
|
+
if (!statusResponse.ok) {
|
|
138
|
+
throw new Error(`Observe status request failed with HTTP ${statusResponse.status}.`);
|
|
139
|
+
}
|
|
140
|
+
const statusPayload = await statusResponse.json();
|
|
141
|
+
const status = getStringAtPath(statusPayload, options.observe.status.statusPath);
|
|
142
|
+
if (!status) {
|
|
143
|
+
throw new Error(`Observe status did not find a status at "${options.observe.status.statusPath}".`);
|
|
144
|
+
}
|
|
145
|
+
await appendObserveEvent(options.outputPath, {
|
|
146
|
+
source: 'observe-status',
|
|
147
|
+
type: 'status',
|
|
148
|
+
status,
|
|
149
|
+
observedAt: new Date().toISOString(),
|
|
150
|
+
});
|
|
151
|
+
if (options.observe.status.successStatuses.includes(status)) {
|
|
152
|
+
return {
|
|
153
|
+
status,
|
|
154
|
+
output: latestOutput,
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
if (options.observe.status.failureStatuses?.includes(status)) {
|
|
158
|
+
throw new run_result_1.MessageRunError(`Observe status reached failure status "${status}".`, {
|
|
159
|
+
threadId: options.fallbackThreadId,
|
|
160
|
+
outputPath: options.outputPath,
|
|
161
|
+
output: latestOutput,
|
|
162
|
+
toolCallCount: options.toolCalls.length,
|
|
163
|
+
toolCalls: options.toolCalls,
|
|
164
|
+
});
|
|
165
|
+
}
|
|
166
|
+
await new Promise((resolve) => setTimeout(resolve, intervalMs));
|
|
167
|
+
}
|
|
168
|
+
throw new run_result_1.MessageRunError(`Observe status timed out after ${options.observe.timeoutMs ?? 300_000}ms.`, {
|
|
169
|
+
threadId: options.fallbackThreadId,
|
|
170
|
+
outputPath: options.outputPath,
|
|
171
|
+
output: latestOutput,
|
|
172
|
+
toolCallCount: options.toolCalls.length,
|
|
173
|
+
toolCalls: options.toolCalls,
|
|
174
|
+
});
|
|
175
|
+
}
|
|
176
|
+
function isRecord(value) {
|
|
177
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
178
|
+
}
|
|
179
|
+
function withInjectedPayload(options) {
|
|
180
|
+
const { payload, message, threadId, runId, injectMessage, injectRunMetadata, } = options;
|
|
181
|
+
if (!isRecord(payload)) {
|
|
182
|
+
throw new Error('ag-ui-post transport requires request.payload to be an object.');
|
|
183
|
+
}
|
|
184
|
+
const body = isRecord(payload.body) ? payload.body : {};
|
|
185
|
+
const existingMessages = Array.isArray(body.messages) ? body.messages : [];
|
|
186
|
+
const firstMessage = isRecord(existingMessages[0]) ? existingMessages[0] : {};
|
|
187
|
+
const resolvedThreadId = threadId ||
|
|
188
|
+
(typeof body.threadId === 'string' && body.threadId.trim() ? body.threadId.trim() : node_crypto_1.default.randomUUID());
|
|
189
|
+
return {
|
|
190
|
+
...payload,
|
|
191
|
+
body: {
|
|
192
|
+
...body,
|
|
193
|
+
...(injectRunMetadata
|
|
194
|
+
? {
|
|
195
|
+
threadId: resolvedThreadId,
|
|
196
|
+
runId,
|
|
197
|
+
}
|
|
198
|
+
: {}),
|
|
199
|
+
...(injectMessage
|
|
200
|
+
? {
|
|
201
|
+
messages: [
|
|
202
|
+
{
|
|
203
|
+
...firstMessage,
|
|
204
|
+
...(injectRunMetadata ? { id: node_crypto_1.default.randomUUID() } : {}),
|
|
205
|
+
content: message ?? firstMessage.content,
|
|
206
|
+
role: 'user',
|
|
207
|
+
},
|
|
208
|
+
],
|
|
209
|
+
}
|
|
210
|
+
: {}),
|
|
211
|
+
},
|
|
212
|
+
};
|
|
213
|
+
}
|
|
214
|
+
function parseSseEvents(rawContent) {
|
|
215
|
+
const normalized = rawContent.replace(/\r\n/g, '\n');
|
|
216
|
+
const blocks = normalized
|
|
217
|
+
.split('\n\n')
|
|
218
|
+
.map((block) => block.trim())
|
|
219
|
+
.filter(Boolean);
|
|
220
|
+
return blocks.map((block) => {
|
|
221
|
+
const lines = block.split('\n');
|
|
222
|
+
let event;
|
|
223
|
+
const dataLines = [];
|
|
224
|
+
for (const line of lines) {
|
|
225
|
+
if (line.startsWith('event:')) {
|
|
226
|
+
event = line.slice('event:'.length).trim();
|
|
227
|
+
continue;
|
|
228
|
+
}
|
|
229
|
+
if (line.startsWith('data:')) {
|
|
230
|
+
dataLines.push(line.slice('data:'.length).trimStart());
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
return {
|
|
234
|
+
event,
|
|
235
|
+
data: dataLines.join('\n'),
|
|
236
|
+
};
|
|
237
|
+
});
|
|
238
|
+
}
|
|
239
|
+
function parseSseBlock(block) {
|
|
240
|
+
const trimmedBlock = block.trim();
|
|
241
|
+
if (!trimmedBlock) {
|
|
242
|
+
return undefined;
|
|
243
|
+
}
|
|
244
|
+
const lines = trimmedBlock.split('\n');
|
|
245
|
+
let event;
|
|
246
|
+
const dataLines = [];
|
|
247
|
+
for (const line of lines) {
|
|
248
|
+
if (line.startsWith('event:')) {
|
|
249
|
+
event = line.slice('event:'.length).trim();
|
|
250
|
+
continue;
|
|
251
|
+
}
|
|
252
|
+
if (line.startsWith('data:')) {
|
|
253
|
+
dataLines.push(line.slice('data:'.length).trimStart());
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
return {
|
|
257
|
+
event,
|
|
258
|
+
data: dataLines.join('\n'),
|
|
259
|
+
};
|
|
260
|
+
}
|
|
261
|
+
function createConnectCollector(options) {
|
|
262
|
+
const assistantFragments = [];
|
|
263
|
+
const toolCalls = [];
|
|
264
|
+
let latestAssistantContent;
|
|
265
|
+
let resolvedThreadId = options.fallbackThreadId;
|
|
266
|
+
let started = false;
|
|
267
|
+
let finished = false;
|
|
268
|
+
let sawAnyEvent = false;
|
|
269
|
+
const consume = (sseEvent) => {
|
|
270
|
+
if (!sseEvent.data) {
|
|
271
|
+
return false;
|
|
272
|
+
}
|
|
273
|
+
let parsed;
|
|
274
|
+
try {
|
|
275
|
+
parsed = JSON.parse(sseEvent.data);
|
|
276
|
+
}
|
|
277
|
+
catch {
|
|
278
|
+
return false;
|
|
279
|
+
}
|
|
280
|
+
sawAnyEvent = true;
|
|
281
|
+
if (typeof parsed.threadId === 'string' && parsed.threadId.trim()) {
|
|
282
|
+
resolvedThreadId = parsed.threadId.trim();
|
|
283
|
+
}
|
|
284
|
+
else if (typeof parsed.conversationId === 'string' && parsed.conversationId.trim()) {
|
|
285
|
+
resolvedThreadId = parsed.conversationId.trim();
|
|
286
|
+
}
|
|
287
|
+
if (parsed.type === 'RUN_STARTED' && parsed.runId === options.targetRunId) {
|
|
288
|
+
started = true;
|
|
289
|
+
return false;
|
|
290
|
+
}
|
|
291
|
+
if (!started) {
|
|
292
|
+
return false;
|
|
293
|
+
}
|
|
294
|
+
if (parsed.type === 'RUN_ERROR' && (!parsed.runId || parsed.runId === options.targetRunId)) {
|
|
295
|
+
throw new run_result_1.MessageRunError(parsed.error?.trim() || 'Agent run failed.', {
|
|
296
|
+
threadId: resolvedThreadId,
|
|
297
|
+
output: latestAssistantContent?.trim() || assistantFragments.join('').trim(),
|
|
298
|
+
toolCallCount: toolCalls.length,
|
|
299
|
+
toolCalls,
|
|
300
|
+
});
|
|
301
|
+
}
|
|
302
|
+
const isAssistantMessage = parsed.role === 'assistant' || typeof parsed.role !== 'string';
|
|
303
|
+
if ((parsed.type === 'TEXT_MESSAGE_CHUNK' || parsed.type === 'TEXT_MESSAGE_CONTENT') && isAssistantMessage) {
|
|
304
|
+
if (typeof parsed.content === 'string' && parsed.content.trim()) {
|
|
305
|
+
latestAssistantContent = parsed.content.trim();
|
|
306
|
+
}
|
|
307
|
+
else if (typeof parsed.delta === 'string' && parsed.delta) {
|
|
308
|
+
assistantFragments.push(parsed.delta);
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
if (parsed.type === 'TOOL_CALL_START' && typeof parsed.toolCallName === 'string' && parsed.toolCallName.trim()) {
|
|
312
|
+
toolCalls.push(parsed.toolCallName.trim());
|
|
313
|
+
}
|
|
314
|
+
if (parsed.type === 'RUN_FINISHED' && (!parsed.runId || parsed.runId === options.targetRunId)) {
|
|
315
|
+
finished = true;
|
|
316
|
+
return true;
|
|
317
|
+
}
|
|
318
|
+
return false;
|
|
319
|
+
};
|
|
320
|
+
return {
|
|
321
|
+
consume,
|
|
322
|
+
getResult() {
|
|
323
|
+
return {
|
|
324
|
+
output: latestAssistantContent?.trim() || assistantFragments.join('').trim(),
|
|
325
|
+
toolCalls: [...toolCalls],
|
|
326
|
+
threadId: resolvedThreadId,
|
|
327
|
+
finished,
|
|
328
|
+
sawAnyEvent,
|
|
329
|
+
};
|
|
330
|
+
},
|
|
331
|
+
};
|
|
332
|
+
}
|
|
333
|
+
async function postAndCaptureResponse(options) {
|
|
334
|
+
const abortController = new AbortController();
|
|
335
|
+
const timeoutId = typeof options.processTimeoutMs === 'number'
|
|
336
|
+
? setTimeout(() => abortController.abort(), options.processTimeoutMs)
|
|
337
|
+
: undefined;
|
|
338
|
+
try {
|
|
339
|
+
await promises_1.default.writeFile(options.outputPath, '', 'utf8');
|
|
340
|
+
const response = await fetch(options.url, {
|
|
341
|
+
method: 'POST',
|
|
342
|
+
headers: {
|
|
343
|
+
accept: options.accept ?? 'text/event-stream',
|
|
344
|
+
'content-type': 'application/json',
|
|
345
|
+
...(options.headers ?? {}),
|
|
346
|
+
},
|
|
347
|
+
body: JSON.stringify(options.payload),
|
|
348
|
+
signal: abortController.signal,
|
|
349
|
+
});
|
|
350
|
+
let rawContent = '';
|
|
351
|
+
if (response.body) {
|
|
352
|
+
const reader = response.body.getReader();
|
|
353
|
+
const decoder = new TextDecoder();
|
|
354
|
+
while (true) {
|
|
355
|
+
const { done, value } = await reader.read();
|
|
356
|
+
if (done) {
|
|
357
|
+
rawContent += decoder.decode();
|
|
358
|
+
break;
|
|
359
|
+
}
|
|
360
|
+
const chunkText = decoder.decode(value, { stream: true });
|
|
361
|
+
rawContent += chunkText;
|
|
362
|
+
await promises_1.default.appendFile(options.outputPath, chunkText, 'utf8');
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
else {
|
|
366
|
+
rawContent = await response.text();
|
|
367
|
+
await promises_1.default.writeFile(options.outputPath, rawContent, 'utf8');
|
|
368
|
+
}
|
|
369
|
+
return {
|
|
370
|
+
status: response.status,
|
|
371
|
+
rawContent,
|
|
372
|
+
};
|
|
373
|
+
}
|
|
374
|
+
catch (error) {
|
|
375
|
+
if (error instanceof Error && error.name === 'AbortError') {
|
|
376
|
+
throw new run_result_1.MessageRunError(`ag-ui-post exceeded ${options.processTimeoutMs}ms and was terminated.`, {
|
|
377
|
+
outputPath: options.outputPath,
|
|
378
|
+
});
|
|
379
|
+
}
|
|
380
|
+
throw error;
|
|
381
|
+
}
|
|
382
|
+
finally {
|
|
383
|
+
if (timeoutId) {
|
|
384
|
+
clearTimeout(timeoutId);
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
function startConnectStream(options) {
|
|
389
|
+
let resolveReady;
|
|
390
|
+
let rejectReady;
|
|
391
|
+
const ready = new Promise((resolve, reject) => {
|
|
392
|
+
resolveReady = resolve;
|
|
393
|
+
rejectReady = reject;
|
|
394
|
+
});
|
|
395
|
+
const result = (async () => {
|
|
396
|
+
const abortController = new AbortController();
|
|
397
|
+
const timeoutId = typeof options.processTimeoutMs === 'number'
|
|
398
|
+
? setTimeout(() => abortController.abort(), options.processTimeoutMs)
|
|
399
|
+
: undefined;
|
|
400
|
+
try {
|
|
401
|
+
await promises_1.default.writeFile(options.outputPath, '', 'utf8');
|
|
402
|
+
const response = await fetch(options.url, {
|
|
403
|
+
method: 'POST',
|
|
404
|
+
headers: {
|
|
405
|
+
accept: 'text/event-stream',
|
|
406
|
+
'content-type': 'application/json',
|
|
407
|
+
...(options.headers ?? {}),
|
|
408
|
+
},
|
|
409
|
+
body: JSON.stringify(options.payload),
|
|
410
|
+
signal: abortController.signal,
|
|
411
|
+
});
|
|
412
|
+
if (!response.ok) {
|
|
413
|
+
throw new run_result_1.MessageRunError(`ag-ui-connect failed with HTTP ${response.status}.`, {
|
|
414
|
+
threadId: options.fallbackThreadId,
|
|
415
|
+
outputPath: options.outputPath,
|
|
416
|
+
});
|
|
417
|
+
}
|
|
418
|
+
resolveReady();
|
|
419
|
+
const collector = createConnectCollector({
|
|
420
|
+
targetRunId: options.targetRunId,
|
|
421
|
+
fallbackThreadId: options.fallbackThreadId,
|
|
422
|
+
});
|
|
423
|
+
if (!response.body) {
|
|
424
|
+
const rawContent = await response.text();
|
|
425
|
+
await promises_1.default.writeFile(options.outputPath, rawContent, 'utf8');
|
|
426
|
+
const parsed = extractResultFromSse(rawContent, options.fallbackThreadId);
|
|
427
|
+
return {
|
|
428
|
+
output: parsed.output,
|
|
429
|
+
toolCalls: parsed.toolCalls,
|
|
430
|
+
threadId: parsed.threadId,
|
|
431
|
+
finished: parsed.finished,
|
|
432
|
+
sawAnyEvent: parsed.sawAnyEvent,
|
|
433
|
+
};
|
|
434
|
+
}
|
|
435
|
+
const reader = response.body.getReader();
|
|
436
|
+
const decoder = new TextDecoder();
|
|
437
|
+
let buffer = '';
|
|
438
|
+
while (true) {
|
|
439
|
+
const { done, value } = await reader.read();
|
|
440
|
+
if (done) {
|
|
441
|
+
buffer += decoder.decode();
|
|
442
|
+
}
|
|
443
|
+
else {
|
|
444
|
+
const chunkText = decoder.decode(value, { stream: true });
|
|
445
|
+
buffer += chunkText;
|
|
446
|
+
await promises_1.default.appendFile(options.outputPath, chunkText, 'utf8');
|
|
447
|
+
}
|
|
448
|
+
buffer = buffer.replace(/\r\n/g, '\n');
|
|
449
|
+
const blocks = buffer.split('\n\n');
|
|
450
|
+
buffer = blocks.pop() ?? '';
|
|
451
|
+
for (const block of blocks) {
|
|
452
|
+
const parsedEvent = parseSseBlock(block);
|
|
453
|
+
if (!parsedEvent) {
|
|
454
|
+
continue;
|
|
455
|
+
}
|
|
456
|
+
const shouldStop = collector.consume(parsedEvent);
|
|
457
|
+
if (shouldStop) {
|
|
458
|
+
await reader.cancel();
|
|
459
|
+
const parsed = collector.getResult();
|
|
460
|
+
return parsed;
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
if (done) {
|
|
464
|
+
break;
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
if (buffer.trim()) {
|
|
468
|
+
const parsedEvent = parseSseBlock(buffer);
|
|
469
|
+
if (parsedEvent) {
|
|
470
|
+
collector.consume(parsedEvent);
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
return collector.getResult();
|
|
474
|
+
}
|
|
475
|
+
catch (error) {
|
|
476
|
+
rejectReady(error);
|
|
477
|
+
if (error instanceof Error && error.name === 'AbortError') {
|
|
478
|
+
throw new run_result_1.MessageRunError(`ag-ui-connect exceeded ${options.processTimeoutMs}ms and was terminated.`, {
|
|
479
|
+
threadId: options.fallbackThreadId,
|
|
480
|
+
outputPath: options.outputPath,
|
|
481
|
+
});
|
|
482
|
+
}
|
|
483
|
+
throw error;
|
|
484
|
+
}
|
|
485
|
+
finally {
|
|
486
|
+
if (timeoutId) {
|
|
487
|
+
clearTimeout(timeoutId);
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
})();
|
|
491
|
+
result.catch(() => { });
|
|
492
|
+
ready.catch(() => { });
|
|
493
|
+
return {
|
|
494
|
+
ready,
|
|
495
|
+
result,
|
|
496
|
+
};
|
|
497
|
+
}
|
|
498
|
+
function extractResultFromSse(rawContent, fallbackThreadId) {
|
|
499
|
+
const fragments = [];
|
|
500
|
+
let latestContent;
|
|
501
|
+
let resolvedThreadId = fallbackThreadId;
|
|
502
|
+
const toolCalls = [];
|
|
503
|
+
let finished = false;
|
|
504
|
+
let sawAnyEvent = false;
|
|
505
|
+
for (const sseEvent of parseSseEvents(rawContent)) {
|
|
506
|
+
if (!sseEvent.data) {
|
|
507
|
+
continue;
|
|
508
|
+
}
|
|
509
|
+
try {
|
|
510
|
+
const parsed = JSON.parse(sseEvent.data);
|
|
511
|
+
sawAnyEvent = true;
|
|
512
|
+
if (typeof parsed.threadId === 'string' && parsed.threadId.trim()) {
|
|
513
|
+
resolvedThreadId = parsed.threadId.trim();
|
|
514
|
+
}
|
|
515
|
+
else if (typeof parsed.conversationId === 'string' && parsed.conversationId.trim()) {
|
|
516
|
+
resolvedThreadId = parsed.conversationId.trim();
|
|
517
|
+
}
|
|
518
|
+
if (parsed.type === 'TEXT_MESSAGE_CONTENT') {
|
|
519
|
+
if (typeof parsed.content === 'string' && parsed.content.trim()) {
|
|
520
|
+
latestContent = parsed.content.trim();
|
|
521
|
+
}
|
|
522
|
+
else if (typeof parsed.delta === 'string' && parsed.delta) {
|
|
523
|
+
fragments.push(parsed.delta);
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
if (parsed.type === 'TOOL_CALL_START' && typeof parsed.toolCallName === 'string' && parsed.toolCallName.trim()) {
|
|
527
|
+
toolCalls.push(parsed.toolCallName.trim());
|
|
528
|
+
}
|
|
529
|
+
if (parsed.type === 'RUN_FINISHED') {
|
|
530
|
+
finished = true;
|
|
531
|
+
}
|
|
532
|
+
if (parsed.type === 'RUN_ERROR') {
|
|
533
|
+
throw new run_result_1.MessageRunError(parsed.error?.trim() || 'Agent run failed.', {
|
|
534
|
+
threadId: resolvedThreadId,
|
|
535
|
+
output: latestContent?.trim() || fragments.join('').trim(),
|
|
536
|
+
toolCallCount: toolCalls.length,
|
|
537
|
+
toolCalls,
|
|
538
|
+
});
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
catch (error) {
|
|
542
|
+
if (error instanceof run_result_1.MessageRunError) {
|
|
543
|
+
throw error;
|
|
544
|
+
}
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
return {
|
|
548
|
+
output: latestContent?.trim() || fragments.join('').trim(),
|
|
549
|
+
toolCalls,
|
|
550
|
+
threadId: resolvedThreadId,
|
|
551
|
+
finished,
|
|
552
|
+
sawAnyEvent,
|
|
553
|
+
};
|
|
554
|
+
}
|
|
555
|
+
async function runAgUiPostMessage(options) {
|
|
556
|
+
await promises_1.default.mkdir(options.outputDirectory, { recursive: true });
|
|
557
|
+
const runRequest = options.run;
|
|
558
|
+
if (!runRequest) {
|
|
559
|
+
throw new Error('ag-ui-post transport requires run config.');
|
|
560
|
+
}
|
|
561
|
+
const runId = node_crypto_1.default.randomUUID();
|
|
562
|
+
const runPayload = withInjectedPayload({
|
|
563
|
+
payload: runRequest.payload,
|
|
564
|
+
message: options.message,
|
|
565
|
+
threadId: options.threadId,
|
|
566
|
+
runId,
|
|
567
|
+
injectMessage: options.injectMessage ?? true,
|
|
568
|
+
injectRunMetadata: options.injectRunMetadata ?? true,
|
|
569
|
+
});
|
|
570
|
+
const runPayloadBody = isRecord(runPayload) && isRecord(runPayload.body) ? runPayload.body : undefined;
|
|
571
|
+
const resolvedThreadId = runPayloadBody && typeof runPayloadBody.threadId === 'string'
|
|
572
|
+
? runPayloadBody.threadId
|
|
573
|
+
: options.threadId ?? node_crypto_1.default.randomUUID();
|
|
574
|
+
const outputPath = node_path_1.default.join(options.outputDirectory, `${resolvedThreadId}-${Date.now()}.sse`);
|
|
575
|
+
const runOutputPath = node_path_1.default.join(options.outputDirectory, `${resolvedThreadId}-${Date.now()}.run.sse`);
|
|
576
|
+
const observePath = node_path_1.default.join(options.outputDirectory, `${resolvedThreadId}-${Date.now()}.observe.jsonl`);
|
|
577
|
+
try {
|
|
578
|
+
if (options.connect) {
|
|
579
|
+
const connectPayload = withInjectedPayload({
|
|
580
|
+
payload: options.connect.payload,
|
|
581
|
+
threadId: resolvedThreadId,
|
|
582
|
+
runId,
|
|
583
|
+
injectMessage: false,
|
|
584
|
+
injectRunMetadata: true,
|
|
585
|
+
});
|
|
586
|
+
const connectStream = startConnectStream({
|
|
587
|
+
url: options.connect.url,
|
|
588
|
+
headers: options.connect.headers,
|
|
589
|
+
payload: connectPayload,
|
|
590
|
+
outputPath,
|
|
591
|
+
targetRunId: runId,
|
|
592
|
+
fallbackThreadId: resolvedThreadId,
|
|
593
|
+
processTimeoutMs: options.connect.processTimeoutMs ?? options.processTimeoutMs,
|
|
594
|
+
});
|
|
595
|
+
await connectStream.ready;
|
|
596
|
+
const runResponse = await postAndCaptureResponse({
|
|
597
|
+
url: runRequest.url,
|
|
598
|
+
headers: runRequest.headers,
|
|
599
|
+
payload: runPayload,
|
|
600
|
+
outputPath: runOutputPath,
|
|
601
|
+
processTimeoutMs: options.processTimeoutMs,
|
|
602
|
+
});
|
|
603
|
+
if (runResponse.status < 200 || runResponse.status >= 300) {
|
|
604
|
+
throw new run_result_1.MessageRunError(`ag-ui-post run failed with HTTP ${runResponse.status}.`, {
|
|
605
|
+
threadId: resolvedThreadId,
|
|
606
|
+
outputPath: runOutputPath,
|
|
607
|
+
output: runResponse.rawContent,
|
|
608
|
+
});
|
|
609
|
+
}
|
|
610
|
+
const connected = await connectStream.result;
|
|
611
|
+
if (!connected.finished && !connected.output) {
|
|
612
|
+
throw new run_result_1.MessageRunError(connected.sawAnyEvent
|
|
613
|
+
? 'ag-ui-connect returned without RUN_FINISHED or assistant output.'
|
|
614
|
+
: 'ag-ui-connect returned no stream events for the submitted turn.', {
|
|
615
|
+
threadId: connected.threadId,
|
|
616
|
+
outputPath,
|
|
617
|
+
toolCallCount: connected.toolCalls.length,
|
|
618
|
+
toolCalls: connected.toolCalls,
|
|
619
|
+
});
|
|
620
|
+
}
|
|
621
|
+
return {
|
|
622
|
+
output: connected.output,
|
|
623
|
+
threadId: connected.threadId,
|
|
624
|
+
outputPath,
|
|
625
|
+
note: `Run log: ${node_path_1.default.basename(runOutputPath)}`,
|
|
626
|
+
toolCallCount: connected.toolCalls.length,
|
|
627
|
+
toolCalls: connected.toolCalls,
|
|
628
|
+
metrics: {},
|
|
629
|
+
};
|
|
630
|
+
}
|
|
631
|
+
const runResponse = await postAndCaptureResponse({
|
|
632
|
+
url: runRequest.url,
|
|
633
|
+
headers: runRequest.headers,
|
|
634
|
+
payload: runPayload,
|
|
635
|
+
outputPath,
|
|
636
|
+
processTimeoutMs: options.processTimeoutMs,
|
|
637
|
+
});
|
|
638
|
+
if (runResponse.status < 200 || runResponse.status >= 300) {
|
|
639
|
+
throw new run_result_1.MessageRunError(`ag-ui-post failed with HTTP ${runResponse.status}.`, {
|
|
640
|
+
threadId: resolvedThreadId,
|
|
641
|
+
outputPath,
|
|
642
|
+
output: runResponse.rawContent,
|
|
643
|
+
});
|
|
644
|
+
}
|
|
645
|
+
const parsed = extractResultFromSse(runResponse.rawContent, resolvedThreadId);
|
|
646
|
+
if (options.observe) {
|
|
647
|
+
await promises_1.default.writeFile(observePath, '', 'utf8');
|
|
648
|
+
const observed = await waitForObservedCompletion({
|
|
649
|
+
observe: options.observe,
|
|
650
|
+
outputPath: observePath,
|
|
651
|
+
fallbackOutput: parsed.output,
|
|
652
|
+
fallbackThreadId: parsed.threadId,
|
|
653
|
+
toolCalls: parsed.toolCalls,
|
|
654
|
+
});
|
|
655
|
+
return {
|
|
656
|
+
output: observed.output,
|
|
657
|
+
threadId: parsed.threadId,
|
|
658
|
+
outputPath,
|
|
659
|
+
note: `Observe status: ${observed.status}. Observe log: ${node_path_1.default.basename(observePath)}`,
|
|
660
|
+
toolCallCount: parsed.toolCalls.length,
|
|
661
|
+
toolCalls: parsed.toolCalls,
|
|
662
|
+
metrics: {},
|
|
663
|
+
};
|
|
664
|
+
}
|
|
665
|
+
if (options.completionCheck) {
|
|
666
|
+
const completion = await waitForCompletionCheck(options.completionCheck);
|
|
667
|
+
return {
|
|
668
|
+
output: parsed.output,
|
|
669
|
+
threadId: parsed.threadId,
|
|
670
|
+
outputPath,
|
|
671
|
+
note: `Completion status: ${completion.status}`,
|
|
672
|
+
toolCallCount: parsed.toolCalls.length,
|
|
673
|
+
toolCalls: parsed.toolCalls,
|
|
674
|
+
metrics: {},
|
|
675
|
+
};
|
|
676
|
+
}
|
|
677
|
+
if (!parsed.finished && !parsed.output) {
|
|
678
|
+
throw new run_result_1.MessageRunError(parsed.sawAnyEvent
|
|
679
|
+
? 'ag-ui-post returned without RUN_FINISHED or assistant output.'
|
|
680
|
+
: 'ag-ui-post returned no stream events, so the turn completion could not be verified.', {
|
|
681
|
+
threadId: parsed.threadId,
|
|
682
|
+
outputPath,
|
|
683
|
+
output: runResponse.rawContent,
|
|
684
|
+
toolCallCount: parsed.toolCalls.length,
|
|
685
|
+
toolCalls: parsed.toolCalls,
|
|
686
|
+
});
|
|
687
|
+
}
|
|
688
|
+
return {
|
|
689
|
+
output: parsed.output,
|
|
690
|
+
threadId: parsed.threadId,
|
|
691
|
+
outputPath,
|
|
692
|
+
toolCallCount: parsed.toolCalls.length,
|
|
693
|
+
toolCalls: parsed.toolCalls,
|
|
694
|
+
metrics: {},
|
|
695
|
+
};
|
|
696
|
+
}
|
|
697
|
+
catch (error) {
|
|
698
|
+
if (error instanceof run_result_1.MessageRunError) {
|
|
699
|
+
throw error;
|
|
700
|
+
}
|
|
701
|
+
throw error;
|
|
702
|
+
}
|
|
703
|
+
}
|