@myagentroam/agent 0.9.174 → 0.9.176
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/agent-core/extensions/model-stream.js +5 -1
- package/dist/agent-core/model/anthropic-messages.js +33 -24
- package/dist/agent-core/model/contracts.d.ts +7 -0
- package/dist/agent-core/model/openai-responses.js +33 -24
- package/dist/agent-core/model/sse.js +59 -9
- package/dist/agent-core/runtime/events.d.ts +1 -1
- package/dist/agent-core/runtime/execution.js +35 -24
- package/dist/agent-core/runtime/model-generation-timing.d.ts +12 -0
- package/dist/agent-core/runtime/model-generation-timing.js +39 -0
- package/package.json +1 -1
|
@@ -59,7 +59,10 @@ function validEvent(event) {
|
|
|
59
59
|
return typeof event.sourceRef === 'string';
|
|
60
60
|
case 'response.started':
|
|
61
61
|
return typeof event.responseId === 'string';
|
|
62
|
+
case 'output.started':
|
|
62
63
|
case 'output.delta':
|
|
64
|
+
case 'output.paused':
|
|
65
|
+
case 'output.completed':
|
|
63
66
|
case 'output.reset':
|
|
64
67
|
return true;
|
|
65
68
|
case 'text.delta':
|
|
@@ -87,7 +90,8 @@ function validEvent(event) {
|
|
|
87
90
|
typeof event.citation.id === 'string' &&
|
|
88
91
|
typeof event.citation.url === 'string');
|
|
89
92
|
case 'usage':
|
|
90
|
-
return (
|
|
93
|
+
return ((event.final === undefined || typeof event.final === 'boolean') &&
|
|
94
|
+
!!event.usage &&
|
|
91
95
|
typeof event.usage === 'object' &&
|
|
92
96
|
Object.values(event.usage).every((value) => value === undefined ||
|
|
93
97
|
(typeof value === 'number' && Number.isFinite(value) && value >= 0)));
|
|
@@ -39,7 +39,7 @@ export class AnthropicMessagesAdapter {
|
|
|
39
39
|
await reportModelAttempt(request, { ...attemptDiagnostic, state: 'STARTED' });
|
|
40
40
|
try {
|
|
41
41
|
for await (const event of this.startAttempt(request, signal)) {
|
|
42
|
-
if (event.type
|
|
42
|
+
if (event.type.startsWith('output.')) {
|
|
43
43
|
resetOutputTiming = true;
|
|
44
44
|
yield event;
|
|
45
45
|
continue;
|
|
@@ -150,18 +150,19 @@ export class AnthropicMessagesAdapter {
|
|
|
150
150
|
if (type === 'message_start') {
|
|
151
151
|
const message = record(event.message);
|
|
152
152
|
responseId = text(message.id) ?? responseId;
|
|
153
|
-
yield { type: 'response.started', responseId };
|
|
153
|
+
yield observeModelStreamEvent({ type: 'response.started', responseId }, modelStreamEventObservedAt(raw));
|
|
154
154
|
const usage = record(message.usage);
|
|
155
155
|
const inputTokens = inputTokenCount(usage.input_tokens);
|
|
156
156
|
const cacheReadTokens = inputTokenCount(usage.cache_read_input_tokens);
|
|
157
157
|
const cacheWriteTokens = inputTokenCount(usage.cache_creation_input_tokens);
|
|
158
158
|
latestInputUsage = inputUsage(inputTokens, cacheReadTokens, cacheWriteTokens);
|
|
159
|
-
yield {
|
|
159
|
+
yield observeModelStreamEvent({
|
|
160
160
|
type: 'usage',
|
|
161
|
+
final: false,
|
|
161
162
|
usage: {
|
|
162
163
|
...latestInputUsage
|
|
163
164
|
}
|
|
164
|
-
};
|
|
165
|
+
}, modelStreamEventObservedAt(raw));
|
|
165
166
|
}
|
|
166
167
|
else if (type === 'content_block_start') {
|
|
167
168
|
const block = record(event.content_block);
|
|
@@ -174,16 +175,18 @@ export class AnthropicMessagesAdapter {
|
|
|
174
175
|
for (const blockIndex of pendingTextBlocks) {
|
|
175
176
|
const pending = blocks.get(blockIndex);
|
|
176
177
|
if (pending?.type === 'text')
|
|
177
|
-
yield {
|
|
178
|
+
yield observeModelStreamEvent({
|
|
178
179
|
type: 'text.completed',
|
|
179
180
|
itemId: pending.id,
|
|
180
181
|
phase: 'commentary',
|
|
181
182
|
text: pending.text
|
|
182
|
-
};
|
|
183
|
+
}, modelStreamEventObservedAt(raw));
|
|
183
184
|
}
|
|
184
185
|
pendingTextBlocks.clear();
|
|
185
186
|
}
|
|
186
187
|
const id = text(block.id) ?? `${responseId}:content-${index}`;
|
|
188
|
+
if (['text', 'thinking', 'redacted_thinking', 'tool_use', 'server_tool_use'].includes(blockType))
|
|
189
|
+
yield observeModelStreamEvent({ type: 'output.started' }, modelStreamEventObservedAt(raw));
|
|
187
190
|
blocks.set(index, {
|
|
188
191
|
type: blockType,
|
|
189
192
|
id,
|
|
@@ -194,18 +197,18 @@ export class AnthropicMessagesAdapter {
|
|
|
194
197
|
...(blockType === 'redacted_thinking' ? { data: block.data } : {})
|
|
195
198
|
});
|
|
196
199
|
if (blockType === 'text' && text(block.text))
|
|
197
|
-
yield {
|
|
200
|
+
yield observeModelStreamEvent({
|
|
198
201
|
type: 'text.delta',
|
|
199
202
|
itemId: id,
|
|
200
203
|
phase: 'commentary',
|
|
201
204
|
delta: text(block.text) ?? ''
|
|
202
|
-
};
|
|
205
|
+
}, modelStreamEventObservedAt(raw));
|
|
203
206
|
else if (blockType === 'server_tool_use') {
|
|
204
207
|
const toolName = text(block.name) ?? 'unknown';
|
|
205
208
|
const kind = toolName === 'web_fetch' ? 'WEB_FETCH' : 'WEB_SEARCH';
|
|
206
209
|
const summary = anthropicHostedInputSummary(record(block.input));
|
|
207
210
|
hostedTools.set(id, { kind, toolName, summary });
|
|
208
|
-
yield {
|
|
211
|
+
yield observeModelStreamEvent({
|
|
209
212
|
type: 'hosted_tool',
|
|
210
213
|
itemId: id,
|
|
211
214
|
kind,
|
|
@@ -213,7 +216,7 @@ export class AnthropicMessagesAdapter {
|
|
|
213
216
|
...(Object.keys(summary).length ? { input: summary } : {}),
|
|
214
217
|
status: 'RUNNING',
|
|
215
218
|
summary
|
|
216
|
-
};
|
|
219
|
+
}, modelStreamEventObservedAt(raw));
|
|
217
220
|
}
|
|
218
221
|
else if (blockType === 'web_search_tool_result' ||
|
|
219
222
|
blockType === 'web_fetch_tool_result') {
|
|
@@ -228,7 +231,7 @@ export class AnthropicMessagesAdapter {
|
|
|
228
231
|
...(Array.isArray(result) && result.length > 20 ? { sourcesTruncated: true } : {})
|
|
229
232
|
};
|
|
230
233
|
hostedTools.set(toolId, { ...hosted, summary });
|
|
231
|
-
yield {
|
|
234
|
+
yield observeModelStreamEvent({
|
|
232
235
|
type: 'hosted_tool',
|
|
233
236
|
itemId: toolId,
|
|
234
237
|
kind: hosted.kind,
|
|
@@ -247,7 +250,7 @@ export class AnthropicMessagesAdapter {
|
|
|
247
250
|
...(summary.sourcesTruncated ? { truncated: true } : {}),
|
|
248
251
|
status: anthropicHostedResultFailed(result) ? 'FAILED' : 'COMPLETED',
|
|
249
252
|
summary
|
|
250
|
-
};
|
|
253
|
+
}, modelStreamEventObservedAt(raw));
|
|
251
254
|
}
|
|
252
255
|
}
|
|
253
256
|
}
|
|
@@ -283,10 +286,10 @@ export class AnthropicMessagesAdapter {
|
|
|
283
286
|
const citation = record(delta.citation);
|
|
284
287
|
const url = text(citation.url);
|
|
285
288
|
if (url)
|
|
286
|
-
yield {
|
|
289
|
+
yield observeModelStreamEvent({
|
|
287
290
|
type: 'citation',
|
|
288
291
|
citation: { id: `${block.id}-citation`, url, title: text(citation.title) }
|
|
289
|
-
};
|
|
292
|
+
}, modelStreamEventObservedAt(raw));
|
|
290
293
|
}
|
|
291
294
|
}
|
|
292
295
|
else if (type === 'content_block_stop') {
|
|
@@ -302,12 +305,13 @@ export class AnthropicMessagesAdapter {
|
|
|
302
305
|
throw new MarAgentError('MAR_AGENT_MODEL_PROTOCOL_ERROR', 'Anthropic returned an invalid or oversized native thinking block.');
|
|
303
306
|
thinkingBytes += Buffer.byteLength(JSON.stringify(item));
|
|
304
307
|
assertAnthropicThinkingBudget(thinkingBytes);
|
|
305
|
-
yield { type: 'response.item', provider: 'ANTHROPIC_MESSAGES', item };
|
|
308
|
+
yield observeModelStreamEvent({ type: 'response.item', provider: 'ANTHROPIC_MESSAGES', item }, modelStreamEventObservedAt(raw));
|
|
306
309
|
}
|
|
307
310
|
if (block.type === 'thinking' && block.text)
|
|
308
|
-
yield { type: 'reasoning.summary', itemId: block.id, text: block.text };
|
|
311
|
+
yield observeModelStreamEvent({ type: 'reasoning.summary', itemId: block.id, text: block.text }, modelStreamEventObservedAt(raw));
|
|
309
312
|
}
|
|
310
313
|
else if (block?.type === 'server_tool_use') {
|
|
314
|
+
yield observeModelStreamEvent({ type: 'output.paused' }, modelStreamEventObservedAt(raw));
|
|
311
315
|
const hosted = hostedTools.get(block.id);
|
|
312
316
|
if (hosted !== undefined) {
|
|
313
317
|
let input;
|
|
@@ -320,7 +324,7 @@ export class AnthropicMessagesAdapter {
|
|
|
320
324
|
const summary = { ...hosted.summary, ...anthropicHostedInputSummary(record(input)) };
|
|
321
325
|
if (summary.query !== hosted.summary.query || summary.url !== hosted.summary.url) {
|
|
322
326
|
hostedTools.set(block.id, { ...hosted, summary });
|
|
323
|
-
yield {
|
|
327
|
+
yield observeModelStreamEvent({
|
|
324
328
|
type: 'hosted_tool',
|
|
325
329
|
itemId: block.id,
|
|
326
330
|
kind: hosted.kind,
|
|
@@ -328,7 +332,7 @@ export class AnthropicMessagesAdapter {
|
|
|
328
332
|
input: anthropicHostedInputSummary(record(input)),
|
|
329
333
|
status: 'RUNNING',
|
|
330
334
|
summary
|
|
331
|
-
};
|
|
335
|
+
}, modelStreamEventObservedAt(raw));
|
|
332
336
|
}
|
|
333
337
|
}
|
|
334
338
|
}
|
|
@@ -341,18 +345,20 @@ export class AnthropicMessagesAdapter {
|
|
|
341
345
|
catch {
|
|
342
346
|
argumentError = 'Tool arguments are invalid JSON.';
|
|
343
347
|
}
|
|
344
|
-
yield {
|
|
348
|
+
yield observeModelStreamEvent({
|
|
345
349
|
type: 'client_tool.call',
|
|
346
350
|
callId: block.id,
|
|
347
351
|
name: block.name ?? 'unknown',
|
|
348
352
|
arguments: args,
|
|
349
353
|
...(argumentError ? { argumentError } : {})
|
|
350
|
-
};
|
|
354
|
+
}, modelStreamEventObservedAt(raw));
|
|
351
355
|
}
|
|
352
356
|
}
|
|
353
357
|
else if (type === 'message_delta') {
|
|
354
358
|
const delta = record(event.delta);
|
|
355
359
|
const reason = text(delta.stop_reason);
|
|
360
|
+
if (reason !== undefined)
|
|
361
|
+
yield observeModelStreamEvent({ type: 'output.completed' }, modelStreamEventObservedAt(raw));
|
|
356
362
|
terminal =
|
|
357
363
|
reason === 'tool_use'
|
|
358
364
|
? 'tool_use'
|
|
@@ -365,7 +371,7 @@ export class AnthropicMessagesAdapter {
|
|
|
365
371
|
for (const blockIndex of pendingTextBlocks) {
|
|
366
372
|
const block = blocks.get(blockIndex);
|
|
367
373
|
if (block?.type === 'text')
|
|
368
|
-
yield { type: 'text.completed', itemId: block.id, phase, text: block.text };
|
|
374
|
+
yield observeModelStreamEvent({ type: 'text.completed', itemId: block.id, phase, text: block.text }, modelStreamEventObservedAt(raw));
|
|
369
375
|
}
|
|
370
376
|
pendingTextBlocks.clear();
|
|
371
377
|
const usage = record(event.usage);
|
|
@@ -376,13 +382,14 @@ export class AnthropicMessagesAdapter {
|
|
|
376
382
|
const zeroPlaceholder = inputTokens === 0 && cacheReadTokens === 0 && cacheWriteTokens === 0;
|
|
377
383
|
if (!zeroPlaceholder)
|
|
378
384
|
latestInputUsage = inputUsage(inputTokens ?? latestInputUsage?.inputTokens, cacheReadTokens ?? latestInputUsage?.cacheReadTokens, cacheWriteTokens ?? latestInputUsage?.cacheWriteTokens);
|
|
379
|
-
yield {
|
|
385
|
+
yield observeModelStreamEvent({
|
|
380
386
|
type: 'usage',
|
|
387
|
+
final: reason !== undefined,
|
|
381
388
|
usage: {
|
|
382
389
|
...latestInputUsage,
|
|
383
390
|
outputTokens: number(usage.output_tokens)
|
|
384
391
|
}
|
|
385
|
-
};
|
|
392
|
+
}, modelStreamEventObservedAt(raw));
|
|
386
393
|
}
|
|
387
394
|
else if (type === 'message_stop') {
|
|
388
395
|
yield observeModelStreamEvent({ type: 'completed', reason: terminal ?? 'completed' }, modelStreamEventObservedAt(raw));
|
|
@@ -400,7 +407,7 @@ export class AnthropicMessagesAdapter {
|
|
|
400
407
|
: {});
|
|
401
408
|
}
|
|
402
409
|
else
|
|
403
|
-
yield { type: 'unknown', nativeType: type };
|
|
410
|
+
yield observeModelStreamEvent({ type: 'unknown', nativeType: type }, modelStreamEventObservedAt(raw));
|
|
404
411
|
}
|
|
405
412
|
if (terminal === undefined)
|
|
406
413
|
throw new MarAgentError('MAR_AGENT_MODEL_PROTOCOL_ERROR', 'Anthropic Messages stream ended without a terminal event.', {
|
|
@@ -463,6 +470,8 @@ function anthropicHostedResultFailed(result) {
|
|
|
463
470
|
return entries.some((entry) => text(record(entry).type)?.endsWith('_tool_result_error'));
|
|
464
471
|
}
|
|
465
472
|
function isSemanticEvent(event) {
|
|
473
|
+
if (event.type.startsWith('output.'))
|
|
474
|
+
return false;
|
|
466
475
|
return event.type !== 'response.started' && event.type !== 'usage';
|
|
467
476
|
}
|
|
468
477
|
export function isAnthropicRecoverableStreamError(error) {
|
|
@@ -119,8 +119,14 @@ export type ModelStopReason = 'completed' | 'tool_use' | 'pause' | 'max_output'
|
|
|
119
119
|
export type ModelEvent = {
|
|
120
120
|
type: 'response.started';
|
|
121
121
|
responseId: string;
|
|
122
|
+
} | {
|
|
123
|
+
type: 'output.started';
|
|
122
124
|
} | {
|
|
123
125
|
type: 'output.delta';
|
|
126
|
+
} | {
|
|
127
|
+
type: 'output.paused';
|
|
128
|
+
} | {
|
|
129
|
+
type: 'output.completed';
|
|
124
130
|
} | {
|
|
125
131
|
type: 'output.reset';
|
|
126
132
|
} | {
|
|
@@ -179,6 +185,7 @@ export type ModelEvent = {
|
|
|
179
185
|
} | {
|
|
180
186
|
type: 'usage';
|
|
181
187
|
usage: ModelUsage;
|
|
188
|
+
final?: boolean;
|
|
182
189
|
} | {
|
|
183
190
|
type: 'unknown';
|
|
184
191
|
nativeType: string;
|
|
@@ -115,7 +115,7 @@ export class OpenAiResponsesAdapter {
|
|
|
115
115
|
});
|
|
116
116
|
try {
|
|
117
117
|
for await (const event of this.startAttempt(request, signal, requestBody, useContinuation, credentialReason, turn)) {
|
|
118
|
-
if (event.type
|
|
118
|
+
if (event.type.startsWith('output.')) {
|
|
119
119
|
resetOutputTiming = true;
|
|
120
120
|
yield event;
|
|
121
121
|
continue;
|
|
@@ -310,10 +310,15 @@ export class OpenAiResponsesAdapter {
|
|
|
310
310
|
for await (const raw of rawEvents) {
|
|
311
311
|
const event = record(raw);
|
|
312
312
|
const type = text(event.type) ?? 'unknown';
|
|
313
|
+
if (type === 'response.output_item.added' &&
|
|
314
|
+
['reasoning', 'message', 'function_call', 'custom_tool_call'].includes(text(record(event.item).type) ?? ''))
|
|
315
|
+
yield observeModelStreamEvent({ type: 'output.started' }, modelStreamEventObservedAt(raw));
|
|
316
|
+
if (type === 'response.web_search_call.searching')
|
|
317
|
+
yield observeModelStreamEvent({ type: 'output.paused' }, modelStreamEventObservedAt(raw));
|
|
313
318
|
if (type === 'response.output_item.done') {
|
|
314
319
|
const item = responseItem(event.item);
|
|
315
320
|
if (item)
|
|
316
|
-
yield { type: 'response.item', provider: 'OPENAI_RESPONSES', item };
|
|
321
|
+
yield observeModelStreamEvent({ type: 'response.item', provider: 'OPENAI_RESPONSES', item }, modelStreamEventObservedAt(raw));
|
|
317
322
|
}
|
|
318
323
|
if (type === 'response.output_item.added' && text(record(event.item).type) === 'message') {
|
|
319
324
|
const item = record(event.item);
|
|
@@ -332,7 +337,7 @@ export class OpenAiResponsesAdapter {
|
|
|
332
337
|
if (completedText !== undefined && !emittedCompletedText.has(itemId)) {
|
|
333
338
|
emittedCompletedText.add(itemId);
|
|
334
339
|
pendingCompletedText.delete(itemId);
|
|
335
|
-
yield { type: 'text.completed', itemId, phase, text: completedText };
|
|
340
|
+
yield observeModelStreamEvent({ type: 'text.completed', itemId, phase, text: completedText }, modelStreamEventObservedAt(raw));
|
|
336
341
|
}
|
|
337
342
|
}
|
|
338
343
|
else if (type === 'response.output_item.added' &&
|
|
@@ -376,10 +381,10 @@ export class OpenAiResponsesAdapter {
|
|
|
376
381
|
yield observeModelStreamEvent({ type: 'output.delta' }, modelStreamEventObservedAt(raw));
|
|
377
382
|
}
|
|
378
383
|
else if (type === 'response.created')
|
|
379
|
-
yield {
|
|
384
|
+
yield observeModelStreamEvent({
|
|
380
385
|
type: 'response.started',
|
|
381
386
|
responseId: responseEventId(event) ?? 'unknown'
|
|
382
|
-
};
|
|
387
|
+
}, modelStreamEventObservedAt(raw));
|
|
383
388
|
else if (type === 'response.output_text.delta')
|
|
384
389
|
yield observeModelStreamEvent({
|
|
385
390
|
type: 'text.delta',
|
|
@@ -397,17 +402,17 @@ export class OpenAiResponsesAdapter {
|
|
|
397
402
|
continue;
|
|
398
403
|
if (phase) {
|
|
399
404
|
emittedCompletedText.add(itemId);
|
|
400
|
-
yield { type: 'text.completed', itemId, phase, text: completedText };
|
|
405
|
+
yield observeModelStreamEvent({ type: 'text.completed', itemId, phase, text: completedText }, modelStreamEventObservedAt(raw));
|
|
401
406
|
}
|
|
402
407
|
else
|
|
403
408
|
pendingCompletedText.set(itemId, completedText);
|
|
404
409
|
}
|
|
405
410
|
else if (type === 'response.reasoning_summary_text.done')
|
|
406
|
-
yield {
|
|
411
|
+
yield observeModelStreamEvent({
|
|
407
412
|
type: 'reasoning.summary',
|
|
408
413
|
itemId: text(event.item_id) ?? 'reasoning',
|
|
409
414
|
text: text(event.text) ?? ''
|
|
410
|
-
};
|
|
415
|
+
}, modelStreamEventObservedAt(raw));
|
|
411
416
|
else if (type === 'response.output_item.done' &&
|
|
412
417
|
text(record(event.item).type) === 'tool_search_call') {
|
|
413
418
|
const item = record(event.item);
|
|
@@ -423,13 +428,13 @@ export class OpenAiResponsesAdapter {
|
|
|
423
428
|
}
|
|
424
429
|
if (!emittedFunctionCalls.has(callId)) {
|
|
425
430
|
emittedFunctionCalls.add(callId);
|
|
426
|
-
yield {
|
|
431
|
+
yield observeModelStreamEvent({
|
|
427
432
|
type: 'client_tool.call',
|
|
428
433
|
callId,
|
|
429
434
|
name: 'tool_search',
|
|
430
435
|
arguments: arguments_,
|
|
431
436
|
...(argumentError ? { argumentError } : {})
|
|
432
|
-
};
|
|
437
|
+
}, modelStreamEventObservedAt(raw));
|
|
433
438
|
}
|
|
434
439
|
}
|
|
435
440
|
else if (type === 'response.function_call_arguments.done' ||
|
|
@@ -453,13 +458,13 @@ export class OpenAiResponsesAdapter {
|
|
|
453
458
|
'unknown';
|
|
454
459
|
if (!emittedFunctionCalls.has(callId)) {
|
|
455
460
|
emittedFunctionCalls.add(callId);
|
|
456
|
-
yield {
|
|
461
|
+
yield observeModelStreamEvent({
|
|
457
462
|
type: 'client_tool.call',
|
|
458
463
|
callId,
|
|
459
464
|
name: text(event.name) ?? text(item.name) ?? known?.name ?? 'unknown',
|
|
460
465
|
arguments: args,
|
|
461
466
|
...(argumentError ? { argumentError } : {})
|
|
462
|
-
};
|
|
467
|
+
}, modelStreamEventObservedAt(raw));
|
|
463
468
|
}
|
|
464
469
|
}
|
|
465
470
|
else if (type === 'response.custom_tool_call_input.done' ||
|
|
@@ -471,13 +476,13 @@ export class OpenAiResponsesAdapter {
|
|
|
471
476
|
const callId = text(event.call_id) ?? text(item.call_id) ?? known?.callId ?? itemId;
|
|
472
477
|
if (!emittedFunctionCalls.has(callId)) {
|
|
473
478
|
emittedFunctionCalls.add(callId);
|
|
474
|
-
yield {
|
|
479
|
+
yield observeModelStreamEvent({
|
|
475
480
|
type: 'client_tool.call',
|
|
476
481
|
callId,
|
|
477
482
|
name: text(event.name) ?? text(item.name) ?? known?.name ?? 'unknown',
|
|
478
483
|
arguments: text(event.input) ?? text(item.input) ?? known?.input ?? '',
|
|
479
484
|
inputMode: 'freeform'
|
|
480
|
-
};
|
|
485
|
+
}, modelStreamEventObservedAt(raw));
|
|
481
486
|
}
|
|
482
487
|
}
|
|
483
488
|
else if (type.includes('web_search_call') ||
|
|
@@ -502,7 +507,7 @@ export class OpenAiResponsesAdapter {
|
|
|
502
507
|
? {}
|
|
503
508
|
: { sourcesTruncated: summary.sourcesTruncated })
|
|
504
509
|
};
|
|
505
|
-
yield {
|
|
510
|
+
yield observeModelStreamEvent({
|
|
506
511
|
type: 'hosted_tool',
|
|
507
512
|
itemId,
|
|
508
513
|
kind,
|
|
@@ -516,13 +521,13 @@ export class OpenAiResponsesAdapter {
|
|
|
516
521
|
? 'COMPLETED'
|
|
517
522
|
: 'RUNNING',
|
|
518
523
|
summary
|
|
519
|
-
};
|
|
524
|
+
}, modelStreamEventObservedAt(raw));
|
|
520
525
|
}
|
|
521
526
|
else if (type === 'response.output_text.annotation.added') {
|
|
522
527
|
const annotation = record(event.annotation);
|
|
523
528
|
const url = text(annotation.url);
|
|
524
529
|
if (url)
|
|
525
|
-
yield {
|
|
530
|
+
yield observeModelStreamEvent({
|
|
526
531
|
type: 'citation',
|
|
527
532
|
citation: {
|
|
528
533
|
id: text(annotation.id) ??
|
|
@@ -530,9 +535,10 @@ export class OpenAiResponsesAdapter {
|
|
|
530
535
|
url,
|
|
531
536
|
...(text(annotation.title) ? { title: text(annotation.title) } : {})
|
|
532
537
|
}
|
|
533
|
-
};
|
|
538
|
+
}, modelStreamEventObservedAt(raw));
|
|
534
539
|
}
|
|
535
540
|
else if (type === 'response.completed' || type === 'response.incomplete') {
|
|
541
|
+
yield observeModelStreamEvent({ type: 'output.completed' }, modelStreamEventObservedAt(raw));
|
|
536
542
|
const responseData = record(event.response);
|
|
537
543
|
if (type === 'response.completed')
|
|
538
544
|
turn.captureCompletedResponseId(responseEventId(event));
|
|
@@ -540,21 +546,22 @@ export class OpenAiResponsesAdapter {
|
|
|
540
546
|
if (emittedCompletedText.has(itemId))
|
|
541
547
|
continue;
|
|
542
548
|
emittedCompletedText.add(itemId);
|
|
543
|
-
yield {
|
|
549
|
+
yield observeModelStreamEvent({
|
|
544
550
|
type: 'text.completed',
|
|
545
551
|
itemId,
|
|
546
552
|
phase: textPhases.get(itemId) ?? 'final_answer',
|
|
547
553
|
text: completedText
|
|
548
|
-
};
|
|
554
|
+
}, modelStreamEventObservedAt(raw));
|
|
549
555
|
}
|
|
550
556
|
pendingCompletedText.clear();
|
|
551
557
|
for (const citation of responseCitations(responseData))
|
|
552
|
-
yield { type: 'citation', citation };
|
|
558
|
+
yield observeModelStreamEvent({ type: 'citation', citation }, modelStreamEventObservedAt(raw));
|
|
553
559
|
const usage = record(responseData.usage);
|
|
554
560
|
const inputDetails = record(usage.input_tokens_details);
|
|
555
561
|
const rolloutBudgetUnits = optionalRolloutBudgetUnits(usage.codex_rollout_budget_units);
|
|
556
|
-
yield {
|
|
562
|
+
yield observeModelStreamEvent({
|
|
557
563
|
type: 'usage',
|
|
564
|
+
final: true,
|
|
558
565
|
usage: {
|
|
559
566
|
inputTokens: number(usage.input_tokens),
|
|
560
567
|
contextInputTokens: number(usage.input_tokens),
|
|
@@ -563,7 +570,7 @@ export class OpenAiResponsesAdapter {
|
|
|
563
570
|
cacheWriteTokens: number(inputDetails.cache_write_tokens),
|
|
564
571
|
...(rolloutBudgetUnits === undefined ? {} : { rolloutBudgetUnits })
|
|
565
572
|
}
|
|
566
|
-
};
|
|
573
|
+
}, modelStreamEventObservedAt(raw));
|
|
567
574
|
yield observeModelStreamEvent({
|
|
568
575
|
type: 'completed',
|
|
569
576
|
reason: type === 'response.incomplete' ? 'max_output' : 'completed'
|
|
@@ -599,7 +606,7 @@ export class OpenAiResponsesAdapter {
|
|
|
599
606
|
});
|
|
600
607
|
}
|
|
601
608
|
else if (!isNonSemanticOpenAiResponsesEvent(type))
|
|
602
|
-
yield { type: 'unknown', nativeType: type };
|
|
609
|
+
yield observeModelStreamEvent({ type: 'unknown', nativeType: type }, modelStreamEventObservedAt(raw));
|
|
603
610
|
}
|
|
604
611
|
}
|
|
605
612
|
catch (error) {
|
|
@@ -1387,6 +1394,8 @@ function webSocketReceiveError(cause) {
|
|
|
1387
1394
|
});
|
|
1388
1395
|
}
|
|
1389
1396
|
function isSemanticEvent(event) {
|
|
1397
|
+
if (event.type.startsWith('output.'))
|
|
1398
|
+
return false;
|
|
1390
1399
|
if (event.type === 'response.started' || event.type === 'usage' || event.type === 'response.item')
|
|
1391
1400
|
return false;
|
|
1392
1401
|
if (event.type === 'text.delta')
|
|
@@ -38,18 +38,66 @@ export async function* parseSse(response, signal, maxBytes = 16 * 1024 * 1024, i
|
|
|
38
38
|
let buffer = '';
|
|
39
39
|
let total = 0;
|
|
40
40
|
let streamCompleted = false;
|
|
41
|
+
const receiverAbort = new AbortController();
|
|
42
|
+
const receiverSignal = AbortSignal.any([signal, receiverAbort.signal]);
|
|
43
|
+
const chunks = [];
|
|
44
|
+
let nextChunk = 0;
|
|
45
|
+
let receivedAll = false;
|
|
46
|
+
let receiverFailure;
|
|
47
|
+
let wake;
|
|
48
|
+
const notify = () => {
|
|
49
|
+
wake?.();
|
|
50
|
+
wake = undefined;
|
|
51
|
+
};
|
|
52
|
+
// Keep transport receipt independent of async SDK publication and persistence.
|
|
53
|
+
// The cumulative response byte limit also bounds the pending raw chunk queue.
|
|
54
|
+
const receive = (async () => {
|
|
55
|
+
try {
|
|
56
|
+
while (true) {
|
|
57
|
+
const { value, done } = await readStreamChunk(reader, receiverSignal, idleTimeoutMs);
|
|
58
|
+
const observedAt = performance.now();
|
|
59
|
+
if (done) {
|
|
60
|
+
streamCompleted = true;
|
|
61
|
+
break;
|
|
62
|
+
}
|
|
63
|
+
total += value.byteLength;
|
|
64
|
+
enforceResponseSize(total, maxBytes);
|
|
65
|
+
if (value.byteLength === 0)
|
|
66
|
+
continue;
|
|
67
|
+
chunks.push({ value, observedAt });
|
|
68
|
+
notify();
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
catch (error) {
|
|
72
|
+
receiverFailure = { error };
|
|
73
|
+
await reader.cancel(error).catch(() => undefined);
|
|
74
|
+
}
|
|
75
|
+
finally {
|
|
76
|
+
receivedAll = true;
|
|
77
|
+
notify();
|
|
78
|
+
}
|
|
79
|
+
})();
|
|
41
80
|
try {
|
|
42
81
|
while (true) {
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
82
|
+
signal.throwIfAborted();
|
|
83
|
+
const chunk = chunks[nextChunk];
|
|
84
|
+
if (chunk === undefined) {
|
|
85
|
+
if (receivedAll) {
|
|
86
|
+
if (receiverFailure !== undefined)
|
|
87
|
+
throw receiverFailure.error;
|
|
88
|
+
break;
|
|
89
|
+
}
|
|
90
|
+
await new Promise((resolve) => {
|
|
91
|
+
wake = resolve;
|
|
92
|
+
});
|
|
93
|
+
continue;
|
|
50
94
|
}
|
|
51
|
-
|
|
52
|
-
|
|
95
|
+
chunks[nextChunk++] = undefined;
|
|
96
|
+
if (nextChunk >= 1_024 && nextChunk * 2 >= chunks.length) {
|
|
97
|
+
chunks.splice(0, nextChunk);
|
|
98
|
+
nextChunk = 0;
|
|
99
|
+
}
|
|
100
|
+
const { value, observedAt } = chunk;
|
|
53
101
|
buffer += decoder.decode(value, { stream: true }).replaceAll('\r\n', '\n');
|
|
54
102
|
let boundary;
|
|
55
103
|
while ((boundary = buffer.indexOf('\n\n')) >= 0) {
|
|
@@ -75,6 +123,8 @@ export async function* parseSse(response, signal, maxBytes = 16 * 1024 * 1024, i
|
|
|
75
123
|
throw new MarAgentError('MAR_AGENT_MODEL_PROTOCOL_ERROR', 'Model SSE ended with an incomplete frame.');
|
|
76
124
|
}
|
|
77
125
|
finally {
|
|
126
|
+
receiverAbort.abort();
|
|
127
|
+
await receive;
|
|
78
128
|
await closeReader(reader, streamCompleted);
|
|
79
129
|
}
|
|
80
130
|
}
|
|
@@ -26,7 +26,7 @@ export type MarAgentEvent = MarAgentEventBase & ({
|
|
|
26
26
|
modelName: string;
|
|
27
27
|
} | {
|
|
28
28
|
type: 'model.performance';
|
|
29
|
-
latencyMs
|
|
29
|
+
latencyMs?: number | undefined;
|
|
30
30
|
outputTokens?: number | undefined;
|
|
31
31
|
outputDurationMs?: number | undefined;
|
|
32
32
|
tokensPerSecond?: number | undefined;
|
|
@@ -5,6 +5,7 @@ import { modelOutputContinuationMessage } from '../prompts/output.js';
|
|
|
5
5
|
import { executeRoundTools } from '../tools/execution.js';
|
|
6
6
|
import { publicHostedToolEvents, publicModelUsage } from './event-projection.js';
|
|
7
7
|
import { PlanOutputPrefix, splitPlanOutput } from './plan-output.js';
|
|
8
|
+
import { ModelGenerationTiming } from './model-generation-timing.js';
|
|
8
9
|
/** Own the single model/tool loop; the SDK owns its handle, terminal events and cleanup. */
|
|
9
10
|
export class ExecutionLoop {
|
|
10
11
|
input;
|
|
@@ -38,8 +39,7 @@ export class ExecutionLoop {
|
|
|
38
39
|
let sawModelSemanticEvent = false;
|
|
39
40
|
let requestStartedAt;
|
|
40
41
|
let firstModelResponseAt;
|
|
41
|
-
let
|
|
42
|
-
let lastOutputDeltaAt;
|
|
42
|
+
let generation = new ModelGenerationTiming();
|
|
43
43
|
let requestCompletedAt;
|
|
44
44
|
let requestOutputTokens;
|
|
45
45
|
const roundMessageCount = messages.length;
|
|
@@ -52,19 +52,18 @@ export class ExecutionLoop {
|
|
|
52
52
|
});
|
|
53
53
|
};
|
|
54
54
|
const emitFinalPerformance = async (completedAt) => {
|
|
55
|
-
if (mode === 'compact'
|
|
56
|
-
firstModelResponseAt === undefined ||
|
|
57
|
-
firstOutputDeltaAt === undefined ||
|
|
58
|
-
lastOutputDeltaAt === undefined ||
|
|
59
|
-
lastOutputDeltaAt - firstOutputDeltaAt < 1)
|
|
55
|
+
if (mode === 'compact')
|
|
60
56
|
return;
|
|
61
|
-
|
|
57
|
+
generation.finish(completedAt);
|
|
58
|
+
const outputDuration = generation.durationMs;
|
|
62
59
|
const outputTokens = requestOutputTokens;
|
|
63
|
-
if (outputTokens === undefined || outputDuration
|
|
60
|
+
if (outputTokens === undefined || outputDuration === undefined)
|
|
64
61
|
return;
|
|
65
62
|
await input.publish({
|
|
66
63
|
type: 'model.performance',
|
|
67
|
-
|
|
64
|
+
...(firstModelResponseAt === undefined
|
|
65
|
+
? {}
|
|
66
|
+
: { latencyMs: Math.max(0, Math.round(firstModelResponseAt - requestStartedAt)) }),
|
|
68
67
|
outputTokens,
|
|
69
68
|
outputDurationMs: Math.round(outputDuration),
|
|
70
69
|
tokensPerSecond: Number(((outputTokens * 1000) / outputDuration).toFixed(2))
|
|
@@ -91,14 +90,25 @@ export class ExecutionLoop {
|
|
|
91
90
|
retryState: modelRetryState
|
|
92
91
|
})) {
|
|
93
92
|
if (event.type === 'output.reset') {
|
|
94
|
-
|
|
95
|
-
|
|
93
|
+
generation = new ModelGenerationTiming();
|
|
94
|
+
requestCompletedAt = undefined;
|
|
96
95
|
requestOutputTokens = undefined;
|
|
97
96
|
continue;
|
|
98
97
|
}
|
|
99
|
-
const
|
|
100
|
-
|
|
101
|
-
|
|
98
|
+
const observedAt = Math.max(requestStartedAt, modelStreamEventObservedAt(event) ?? performance.now());
|
|
99
|
+
if (event.type === 'output.started') {
|
|
100
|
+
generation.start(observedAt);
|
|
101
|
+
continue;
|
|
102
|
+
}
|
|
103
|
+
if (event.type === 'output.completed') {
|
|
104
|
+
generation.finish(observedAt);
|
|
105
|
+
continue;
|
|
106
|
+
}
|
|
107
|
+
if (event.type === 'output.paused') {
|
|
108
|
+
generation.pause(observedAt);
|
|
109
|
+
continue;
|
|
110
|
+
}
|
|
111
|
+
const modelResponseAt = mode !== 'compact' && isModelResponseEvent(event) ? observedAt : undefined;
|
|
102
112
|
const isFirstModelResponse = modelResponseAt !== undefined && firstModelResponseAt === undefined;
|
|
103
113
|
if (modelResponseAt !== undefined) {
|
|
104
114
|
if (event.type !== 'output.delta')
|
|
@@ -106,8 +116,7 @@ export class ExecutionLoop {
|
|
|
106
116
|
firstModelResponseAt ??= modelResponseAt;
|
|
107
117
|
if (event.type === 'output.delta' ||
|
|
108
118
|
(event.type === 'text.delta' && event.delta.length > 0)) {
|
|
109
|
-
|
|
110
|
-
lastOutputDeltaAt = modelResponseAt;
|
|
119
|
+
generation.start(modelResponseAt);
|
|
111
120
|
}
|
|
112
121
|
}
|
|
113
122
|
if (event.type === 'response.item') {
|
|
@@ -185,14 +194,15 @@ export class ExecutionLoop {
|
|
|
185
194
|
}
|
|
186
195
|
else if (event.type === 'usage') {
|
|
187
196
|
latestUsage = event.usage;
|
|
197
|
+
if (event.final === true)
|
|
198
|
+
generation.finish(observedAt);
|
|
188
199
|
const outputTokens = typeof event.usage.outputTokens === 'number' &&
|
|
189
200
|
Number.isSafeInteger(event.usage.outputTokens) &&
|
|
190
201
|
event.usage.outputTokens >= 0
|
|
191
202
|
? event.usage.outputTokens
|
|
192
203
|
: undefined;
|
|
193
|
-
if (outputTokens !== undefined)
|
|
204
|
+
if (event.final !== false && (outputTokens !== undefined || event.final === true))
|
|
194
205
|
requestOutputTokens = outputTokens;
|
|
195
|
-
}
|
|
196
206
|
const reportedContext = event.usage.contextInputTokens ?? event.usage.inputTokens;
|
|
197
207
|
if (reportedContext !== undefined &&
|
|
198
208
|
Number.isSafeInteger(reportedContext) &&
|
|
@@ -235,15 +245,16 @@ export class ExecutionLoop {
|
|
|
235
245
|
}
|
|
236
246
|
else if (event.type === 'completed') {
|
|
237
247
|
stop = event.reason;
|
|
238
|
-
requestCompletedAt =
|
|
248
|
+
requestCompletedAt = observedAt;
|
|
239
249
|
}
|
|
240
250
|
if (isFirstModelResponse)
|
|
241
251
|
await emitLatency();
|
|
242
252
|
}
|
|
243
|
-
if (mode !== 'compact' &&
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
253
|
+
if (mode !== 'compact' &&
|
|
254
|
+
stop !== 'cancelled' &&
|
|
255
|
+
!input.signal.aborted &&
|
|
256
|
+
requestCompletedAt !== undefined)
|
|
257
|
+
await emitFinalPerformance(requestCompletedAt);
|
|
247
258
|
pendingOutputContinuation = undefined;
|
|
248
259
|
incompleteToolCall = pendingTools.some((call) => call.argumentError !== undefined);
|
|
249
260
|
if (!(stop === 'max_output' && incompleteToolCall)) {
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/** Request-local observed generation intervals, excluding explicit execution pauses. */
|
|
2
|
+
export declare class ModelGenerationTiming {
|
|
3
|
+
private startedAt;
|
|
4
|
+
private latestAt;
|
|
5
|
+
private duration;
|
|
6
|
+
private completed;
|
|
7
|
+
private valid;
|
|
8
|
+
start(at: number): void;
|
|
9
|
+
pause(at: number): void;
|
|
10
|
+
finish(at: number): void;
|
|
11
|
+
get durationMs(): number | undefined;
|
|
12
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/** Request-local observed generation intervals, excluding explicit execution pauses. */
|
|
2
|
+
export class ModelGenerationTiming {
|
|
3
|
+
startedAt;
|
|
4
|
+
latestAt;
|
|
5
|
+
duration = 0;
|
|
6
|
+
completed = false;
|
|
7
|
+
valid = true;
|
|
8
|
+
start(at) {
|
|
9
|
+
if (this.completed)
|
|
10
|
+
return;
|
|
11
|
+
if (!Number.isFinite(at) || (this.latestAt !== undefined && at < this.latestAt)) {
|
|
12
|
+
this.valid = false;
|
|
13
|
+
return;
|
|
14
|
+
}
|
|
15
|
+
this.startedAt ??= at;
|
|
16
|
+
this.latestAt = at;
|
|
17
|
+
}
|
|
18
|
+
pause(at) {
|
|
19
|
+
if (this.completed || this.startedAt === undefined)
|
|
20
|
+
return;
|
|
21
|
+
if (!Number.isFinite(at) || at < (this.latestAt ?? this.startedAt))
|
|
22
|
+
this.valid = false;
|
|
23
|
+
else
|
|
24
|
+
this.duration += at - this.startedAt;
|
|
25
|
+
this.startedAt = undefined;
|
|
26
|
+
this.latestAt = at;
|
|
27
|
+
}
|
|
28
|
+
finish(at) {
|
|
29
|
+
if (this.completed)
|
|
30
|
+
return;
|
|
31
|
+
if (!Number.isFinite(at) || (this.latestAt !== undefined && at < this.latestAt))
|
|
32
|
+
this.valid = false;
|
|
33
|
+
this.pause(at);
|
|
34
|
+
this.completed = true;
|
|
35
|
+
}
|
|
36
|
+
get durationMs() {
|
|
37
|
+
return this.completed && this.valid && this.duration >= 1 ? this.duration : undefined;
|
|
38
|
+
}
|
|
39
|
+
}
|