@myagentroam/agent 0.9.174 → 0.9.175

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.
@@ -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 (!!event.usage &&
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 === 'output.delta') {
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 === 'output.delta') {
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
- if (signal.aborted)
44
- throw signal.reason;
45
- const { value, done } = await readStreamChunk(reader, signal, idleTimeoutMs);
46
- const observedAt = performance.now();
47
- if (done) {
48
- streamCompleted = true;
49
- break;
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
- total += value.byteLength;
52
- enforceResponseSize(total, maxBytes);
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: number;
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 firstOutputDeltaAt;
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
- const outputDuration = completedAt - firstOutputDeltaAt;
57
+ generation.finish(completedAt);
58
+ const outputDuration = generation.durationMs;
62
59
  const outputTokens = requestOutputTokens;
63
- if (outputTokens === undefined || outputDuration < 1 || completedAt < lastOutputDeltaAt)
60
+ if (outputTokens === undefined || outputDuration === undefined)
64
61
  return;
65
62
  await input.publish({
66
63
  type: 'model.performance',
67
- latencyMs: Math.max(0, Math.round(firstModelResponseAt - requestStartedAt)),
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
- firstOutputDeltaAt = undefined;
95
- lastOutputDeltaAt = undefined;
93
+ generation = new ModelGenerationTiming();
94
+ requestCompletedAt = undefined;
96
95
  requestOutputTokens = undefined;
97
96
  continue;
98
97
  }
99
- const modelResponseAt = mode !== 'compact' && isModelResponseEvent(event)
100
- ? Math.max(requestStartedAt, modelStreamEventObservedAt(event) ?? performance.now())
101
- : undefined;
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
- firstOutputDeltaAt ??= modelResponseAt;
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 = modelStreamEventObservedAt(event) ?? performance.now();
248
+ requestCompletedAt = observedAt;
239
249
  }
240
250
  if (isFirstModelResponse)
241
251
  await emitLatency();
242
252
  }
243
- if (mode !== 'compact' && firstModelResponseAt !== undefined) {
244
- const completedAt = requestCompletedAt ?? performance.now();
245
- await emitFinalPerformance(completedAt);
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
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@myagentroam/agent",
3
- "version": "0.9.174",
3
+ "version": "0.9.175",
4
4
  "description": "Embeddable MAR coding agent SDK and CLI.",
5
5
  "license": "Apache-2.0",
6
6
  "type": "module",