@myagentroam/agent 0.9.140 → 0.9.141

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.
@@ -1,7 +1,7 @@
1
1
  import { randomUUID } from 'node:crypto';
2
2
  import { MarAgentError } from '../error.js';
3
3
  import { resolveModelDefaultReasoningEffort } from './configuration.js';
4
- import { isRetryableUpstreamOverloadCode, number, parseSse, record, text } from './sse.js';
4
+ import { isRetryableUpstreamOverloadCode, number, parseSse, record, text, observeModelStreamEvent, modelStreamEventObservedAt } from './sse.js';
5
5
  import { fetchModelResponse, modelRetryMaxAttempts, isRetryableModelRequestError, isRetryableModelTransportError, waitForModelRetry } from './http.js';
6
6
  import { anthropicThinkingBlock, assertAnthropicThinkingBudget } from './anthropic-thinking.js';
7
7
  export class AnthropicMessagesAdapter {
@@ -12,13 +12,22 @@ export class AnthropicMessagesAdapter {
12
12
  async *start(request, signal) {
13
13
  const retryState = request.retryState ?? { attempts: 0 };
14
14
  const maxAttempts = modelRetryMaxAttempts(retryState);
15
+ let resetOutputTiming = false;
15
16
  while (retryState.attempts < maxAttempts) {
17
+ if (resetOutputTiming)
18
+ yield { type: 'output.reset' };
19
+ resetOutputTiming = false;
16
20
  const attempt = retryState.attempts++;
17
21
  // Buffer non-semantic events until a semantic event makes this attempt non-retryable.
18
22
  const prelude = [];
19
23
  let sawSemanticEvent = false;
20
24
  try {
21
25
  for await (const event of this.startAttempt(request, signal)) {
26
+ if (event.type === 'output.delta') {
27
+ resetOutputTiming = true;
28
+ yield event;
29
+ continue;
30
+ }
22
31
  if (!sawSemanticEvent && !isSemanticEvent(event)) {
23
32
  prelude.push(event);
24
33
  continue;
@@ -193,17 +202,25 @@ export class AnthropicMessagesAdapter {
193
202
  if (deltaType === 'text_delta') {
194
203
  const value = text(delta.text) ?? '';
195
204
  block.text += value;
196
- yield { type: 'text.delta', itemId: block.id, phase: 'commentary', delta: value };
205
+ yield observeModelStreamEvent({ type: 'text.delta', itemId: block.id, phase: 'commentary', delta: value }, modelStreamEventObservedAt(raw));
206
+ }
207
+ else if (deltaType === 'thinking_delta') {
208
+ const value = text(delta.thinking) ?? '';
209
+ block.text += value;
210
+ if (value.length > 0)
211
+ yield observeModelStreamEvent({ type: 'output.delta' }, modelStreamEventObservedAt(raw));
197
212
  }
198
- else if (deltaType === 'thinking_delta')
199
- block.text += text(delta.thinking) ?? '';
200
213
  else if (deltaType === 'signature_delta') {
201
214
  if (block.type !== 'thinking' || typeof delta.signature !== 'string')
202
215
  throw new MarAgentError('MAR_AGENT_MODEL_PROTOCOL_ERROR', 'Anthropic returned an invalid thinking signature delta.');
203
216
  block.signature += delta.signature;
204
217
  }
205
- else if (deltaType === 'input_json_delta')
206
- block.json += text(delta.partial_json) ?? '';
218
+ else if (deltaType === 'input_json_delta') {
219
+ const value = text(delta.partial_json) ?? '';
220
+ block.json += value;
221
+ if (value.length > 0)
222
+ yield observeModelStreamEvent({ type: 'output.delta' }, modelStreamEventObservedAt(raw));
223
+ }
207
224
  else if (deltaType === 'citations_delta') {
208
225
  const citation = record(delta.citation);
209
226
  const url = text(citation.url);
@@ -307,7 +324,7 @@ export class AnthropicMessagesAdapter {
307
324
  };
308
325
  }
309
326
  else if (type === 'message_stop') {
310
- yield { type: 'completed', reason: terminal ?? 'completed' };
327
+ yield observeModelStreamEvent({ type: 'completed', reason: terminal ?? 'completed' }, modelStreamEventObservedAt(raw));
311
328
  }
312
329
  else if (type === 'error') {
313
330
  const upstream = record(event.error);
@@ -95,6 +95,10 @@ export type ModelStopReason = 'completed' | 'tool_use' | 'pause' | 'max_output'
95
95
  export type ModelEvent = {
96
96
  type: 'response.started';
97
97
  responseId: string;
98
+ } | {
99
+ type: 'output.delta';
100
+ } | {
101
+ type: 'output.reset';
98
102
  } | {
99
103
  type: 'response.item';
100
104
  provider: 'OPENAI_RESPONSES' | 'ANTHROPIC_MESSAGES';
@@ -3,7 +3,7 @@ import { createHash, randomUUID } from 'node:crypto';
3
3
  import { describeMarAgentError, MarAgentError } from '../error.js';
4
4
  import { marAgentReasoningEffortSchema, resolveModelDefaultReasoningEffort, usesResponsesConfigurationUpdates } from './configuration.js';
5
5
  import WebSocket from 'ws';
6
- import { isRetryableUpstreamOverloadCode, number, parseSse, record, text } from './sse.js';
6
+ import { isRetryableUpstreamOverloadCode, number, parseSse, record, text, observeModelStreamEvent, modelStreamEventObservedAt } from './sse.js';
7
7
  import { connectSocks5Target, fetchModelResponse, isRetryableModelHttpStatus, isRetryableModelTransportError, modelRetryMaxAttempts, isRetryableModelRequestError, modelRetryDelay, waitForModelRetry } from './http.js';
8
8
  import { acquireModelCredential, appendModelCredentialHeaders } from './runtime-credential.js';
9
9
  const WEBSOCKET_IDLE_TIMEOUT_MS = 300_000;
@@ -77,7 +77,11 @@ export class OpenAiResponsesAdapter {
77
77
  let invalidContinuationFallbackUsed = false;
78
78
  const retryState = request.retryState ?? { attempts: 0 };
79
79
  const maxAttempts = modelRetryMaxAttempts(retryState);
80
+ let resetOutputTiming = false;
80
81
  while (retryState.attempts < maxAttempts) {
82
+ if (resetOutputTiming)
83
+ yield { type: 'output.reset' };
84
+ resetOutputTiming = false;
81
85
  const attempt = retryState.attempts++;
82
86
  const attemptNumber = attempt + 1;
83
87
  const attemptStartedAt = Date.now();
@@ -99,6 +103,11 @@ export class OpenAiResponsesAdapter {
99
103
  });
100
104
  try {
101
105
  for await (const event of this.startAttempt(request, signal, requestBody, useContinuation, credentialReason, turn)) {
106
+ if (event.type === 'output.delta') {
107
+ resetOutputTiming = true;
108
+ yield event;
109
+ continue;
110
+ }
102
111
  if (!sawSemanticEvent && !isSemanticEvent(event)) {
103
112
  prelude.push(event);
104
113
  continue;
@@ -341,8 +350,16 @@ export class OpenAiResponsesAdapter {
341
350
  else if (type === 'response.custom_tool_call_input.delta') {
342
351
  const itemId = text(event.item_id) ?? '';
343
352
  const known = customCalls.get(itemId);
353
+ const delta = text(event.delta) ?? '';
344
354
  if (known)
345
- known.input += text(event.delta) ?? '';
355
+ known.input += delta;
356
+ if (delta.length > 0)
357
+ yield observeModelStreamEvent({ type: 'output.delta' }, modelStreamEventObservedAt(raw));
358
+ }
359
+ else if (type === 'response.function_call_arguments.delta' ||
360
+ type === 'response.reasoning_summary_text.delta') {
361
+ if ((text(event.delta)?.length ?? 0) > 0)
362
+ yield observeModelStreamEvent({ type: 'output.delta' }, modelStreamEventObservedAt(raw));
346
363
  }
347
364
  else if (type === 'response.created')
348
365
  yield {
@@ -350,14 +367,14 @@ export class OpenAiResponsesAdapter {
350
367
  responseId: responseEventId(event) ?? 'unknown'
351
368
  };
352
369
  else if (type === 'response.output_text.delta')
353
- yield {
370
+ yield observeModelStreamEvent({
354
371
  type: 'text.delta',
355
372
  itemId: text(event.item_id) ?? 'text',
356
373
  phase: messagePhase(event.phase) ??
357
374
  textPhases.get(text(event.item_id) ?? '') ??
358
375
  'commentary',
359
376
  delta: text(event.delta) ?? ''
360
- };
377
+ }, modelStreamEventObservedAt(raw));
361
378
  else if (type === 'response.output_text.done') {
362
379
  const itemId = text(event.item_id) ?? 'text';
363
380
  const completedText = text(event.text) ?? '';
@@ -519,10 +536,10 @@ export class OpenAiResponsesAdapter {
519
536
  ...(rolloutBudgetUnits === undefined ? {} : { rolloutBudgetUnits })
520
537
  }
521
538
  };
522
- yield {
539
+ yield observeModelStreamEvent({
523
540
  type: 'completed',
524
541
  reason: type === 'response.incomplete' ? 'max_output' : 'completed'
525
- };
542
+ }, modelStreamEventObservedAt(raw));
526
543
  completed = true;
527
544
  }
528
545
  else if (type === 'error' || type === 'response.failed') {
@@ -634,6 +651,7 @@ class OpenAiResponsesTurnSession {
634
651
  if (event.type === 'completed')
635
652
  warmCompleted = event.reason === 'completed';
636
653
  if (event.type === 'client_tool.call' ||
654
+ event.type === 'output.delta' ||
637
655
  event.type === 'text.delta' ||
638
656
  event.type === 'text.completed' ||
639
657
  event.type === 'response.item' ||
@@ -660,6 +678,7 @@ class OpenAiResponsesTurnSession {
660
678
  else if (event.type === 'response.item')
661
679
  outputItems.push(event.item);
662
680
  else if (event.type === 'text.delta' ||
681
+ event.type === 'output.delta' ||
663
682
  event.type === 'text.completed' ||
664
683
  event.type === 'reasoning.summary' ||
665
684
  event.type === 'client_tool.call' ||
@@ -1179,7 +1198,8 @@ async function* webSocketMessages(socket, signal) {
1179
1198
  };
1180
1199
  const message = (value) => push({
1181
1200
  type: 'message',
1182
- value
1201
+ value,
1202
+ observedAt: performance.now()
1183
1203
  });
1184
1204
  const close = () => push({ type: 'closed' });
1185
1205
  const error = (cause) => push({
@@ -1228,7 +1248,7 @@ async function* webSocketMessages(socket, signal) {
1228
1248
  const event = JSON.parse(text);
1229
1249
  if (!isWebSocketNonProgressEvent(event))
1230
1250
  idleDeadline = Date.now() + WEBSOCKET_IDLE_TIMEOUT_MS;
1231
- yield event;
1251
+ yield observeModelStreamEvent(event, item.observedAt);
1232
1252
  }
1233
1253
  catch (cause) {
1234
1254
  throw new MarAgentError('MAR_AGENT_MODEL_PROTOCOL_ERROR', 'Model WebSocket contained invalid JSON.', { cause });
@@ -1336,13 +1356,11 @@ const NON_SEMANTIC_OPENAI_RESPONSES_EVENTS = new Set([
1336
1356
  'codex.response.metadata',
1337
1357
  'response.content_part.added',
1338
1358
  'response.content_part.done',
1339
- 'response.function_call_arguments.delta',
1340
1359
  'response.in_progress',
1341
1360
  'response.metadata',
1342
1361
  'response.queued',
1343
1362
  'response.reasoning_summary_part.added',
1344
1363
  'response.reasoning_summary_part.done',
1345
- 'response.reasoning_summary_text.delta',
1346
1364
  'responsesapi.websocket_timing'
1347
1365
  ]);
1348
1366
  function isRetryableResponsesUpstreamError(code, message) {
@@ -1,3 +1,5 @@
1
+ export declare function observeModelStreamEvent<const T>(value: T, observedAt: number | undefined): T;
2
+ export declare function modelStreamEventObservedAt(value: unknown): number | undefined;
1
3
  export declare function parseSse(response: Response, signal: AbortSignal, maxBytes?: number, idleTimeoutMs?: number): AsyncIterable<unknown>;
2
4
  export declare function isRetryableUpstreamOverloadCode(code: string | undefined): boolean;
3
5
  export declare function record(value: unknown): Record<string, unknown>;
package/dist/model/sse.js CHANGED
@@ -1,6 +1,15 @@
1
1
  import { MarAgentError } from '../error.js';
2
2
  import { isRetryableModelHttpStatus } from './http.js';
3
3
  const DEFAULT_MODEL_STREAM_IDLE_TIMEOUT_MS = 300_000;
4
+ const streamEventObservedAt = new WeakMap();
5
+ export function observeModelStreamEvent(value, observedAt) {
6
+ if (typeof value === 'object' && value !== null && observedAt !== undefined)
7
+ streamEventObservedAt.set(value, observedAt);
8
+ return value;
9
+ }
10
+ export function modelStreamEventObservedAt(value) {
11
+ return typeof value === 'object' && value !== null ? streamEventObservedAt.get(value) : undefined;
12
+ }
4
13
  export async function* parseSse(response, signal, maxBytes = 16 * 1024 * 1024, idleTimeoutMs = DEFAULT_MODEL_STREAM_IDLE_TIMEOUT_MS) {
5
14
  if (!response.ok) {
6
15
  const body = await readResponseText(response, signal, maxBytes, idleTimeoutMs);
@@ -34,6 +43,7 @@ export async function* parseSse(response, signal, maxBytes = 16 * 1024 * 1024, i
34
43
  if (signal.aborted)
35
44
  throw signal.reason;
36
45
  const { value, done } = await readStreamChunk(reader, signal, idleTimeoutMs);
46
+ const observedAt = performance.now();
37
47
  if (done) {
38
48
  streamCompleted = true;
39
49
  break;
@@ -53,7 +63,7 @@ export async function* parseSse(response, signal, maxBytes = 16 * 1024 * 1024, i
53
63
  if (data.length === 0 || data === '[DONE]')
54
64
  continue;
55
65
  try {
56
- yield JSON.parse(data);
66
+ yield observeModelStreamEvent(JSON.parse(data), observedAt);
57
67
  }
58
68
  catch (cause) {
59
69
  throw new MarAgentError('MAR_AGENT_MODEL_PROTOCOL_ERROR', 'Model SSE contained invalid JSON.', { cause });
package/dist/sdk/agent.js CHANGED
@@ -37,6 +37,7 @@ import { createCodeToolDispatch } from '../tools/code-mode/dispatch.js';
37
37
  import { createSessionToolScheduler, scheduleToolCallBatch } from '../tools/scheduler.js';
38
38
  import { projectCodeModeSearchResult, projectCodeModeTools } from '../tools/code-mode.js';
39
39
  import { allowsDirectTool, allowsToolCaller, toolCallers } from '../tools/callers.js';
40
+ import { modelStreamEventObservedAt } from '../model/sse.js';
40
41
  import { SubagentSessionController } from '../subagent/session-controller.js';
41
42
  /** Retry only classified recoverable failures within the attempt budget; never retry cancellation. */
42
43
  async function recoverModelStream(error, retryState, signal) {
@@ -905,6 +906,8 @@ export async function createMarAgentWithAdapter(options) {
905
906
  let sawModelSemanticEvent = false;
906
907
  let requestStartedAt;
907
908
  let firstModelResponseAt;
909
+ let firstOutputDeltaAt;
910
+ let lastOutputDeltaAt;
908
911
  let requestCompletedAt;
909
912
  let requestOutputTokens;
910
913
  const roundMessageCount = messages.length;
@@ -917,18 +920,24 @@ export async function createMarAgentWithAdapter(options) {
917
920
  });
918
921
  };
919
922
  const emitFinalPerformance = async (completedAt) => {
920
- if (mode === 'compact' || firstModelResponseAt === undefined)
923
+ if (mode === 'compact' ||
924
+ firstModelResponseAt === undefined ||
925
+ firstOutputDeltaAt === undefined ||
926
+ lastOutputDeltaAt === undefined ||
927
+ lastOutputDeltaAt - firstOutputDeltaAt < 1)
921
928
  return;
922
- const outputDurationMs = Math.max(0, Math.round(completedAt - firstModelResponseAt));
929
+ const outputDuration = completedAt - firstOutputDeltaAt;
923
930
  const outputTokens = requestOutputTokens;
924
- if (outputTokens === undefined || outputDurationMs <= 0)
931
+ if (outputTokens === undefined ||
932
+ outputDuration < 1 ||
933
+ completedAt < lastOutputDeltaAt)
925
934
  return;
926
935
  await emit({
927
936
  type: 'model.performance',
928
937
  latencyMs: Math.max(0, Math.round(firstModelResponseAt - requestStartedAt)),
929
938
  outputTokens,
930
- outputDurationMs,
931
- tokensPerSecond: Number(((outputTokens * 1000) / outputDurationMs).toFixed(2))
939
+ outputDurationMs: Math.round(outputDuration),
940
+ tokensPerSecond: Number(((outputTokens * 1000) / outputDuration).toFixed(2))
932
941
  });
933
942
  };
934
943
  const persistToolCalls = async (calls) => {
@@ -976,11 +985,25 @@ export async function createMarAgentWithAdapter(options) {
976
985
  ...(modelInputImages?.length ? { images: modelInputImages } : {}),
977
986
  onAttemptDiagnostic: recordModelAttempt(mode === 'compact' ? 'COMPACTION' : 'EXECUTION')
978
987
  })) {
979
- const modelResponseAt = mode !== 'compact' && isModelResponseEvent(event) ? performance.now() : undefined;
988
+ if (event.type === 'output.reset') {
989
+ firstOutputDeltaAt = undefined;
990
+ lastOutputDeltaAt = undefined;
991
+ requestOutputTokens = undefined;
992
+ continue;
993
+ }
994
+ const modelResponseAt = mode !== 'compact' && isModelResponseEvent(event)
995
+ ? Math.max(requestStartedAt, modelStreamEventObservedAt(event) ?? performance.now())
996
+ : undefined;
980
997
  const isFirstModelResponse = modelResponseAt !== undefined && firstModelResponseAt === undefined;
981
998
  if (modelResponseAt !== undefined) {
982
- sawModelSemanticEvent = true;
999
+ if (event.type !== 'output.delta')
1000
+ sawModelSemanticEvent = true;
983
1001
  firstModelResponseAt ??= modelResponseAt;
1002
+ if (event.type === 'output.delta' ||
1003
+ (event.type === 'text.delta' && event.delta.length > 0)) {
1004
+ firstOutputDeltaAt ??= modelResponseAt;
1005
+ lastOutputDeltaAt = modelResponseAt;
1006
+ }
984
1007
  }
985
1008
  if (event.type === 'response.item') {
986
1009
  // Compaction keeps only its successful checkpoint, never intermediate native output.
@@ -1117,7 +1140,7 @@ export async function createMarAgentWithAdapter(options) {
1117
1140
  }
1118
1141
  else if (event.type === 'completed') {
1119
1142
  stop = event.reason;
1120
- requestCompletedAt = performance.now();
1143
+ requestCompletedAt = modelStreamEventObservedAt(event) ?? performance.now();
1121
1144
  }
1122
1145
  if (isFirstModelResponse)
1123
1146
  await emitLatency();
@@ -2459,6 +2482,7 @@ function isModelResponseEvent(event) {
2459
2482
  if (event.type === 'text.completed' || event.type === 'reasoning.summary')
2460
2483
  return event.text.length > 0;
2461
2484
  return (event.type === 'response.item' ||
2485
+ event.type === 'output.delta' ||
2462
2486
  event.type === 'client_tool.call' ||
2463
2487
  event.type === 'hosted_tool' ||
2464
2488
  event.type === 'citation');
@@ -527,13 +527,23 @@ function streamText(value, emit) {
527
527
  else {
528
528
  emit('{');
529
529
  let first = true;
530
- for (const name of Object.keys(item)) {
530
+ const names = Object.keys(item);
531
+ // Special-case view_image and native MCP image objects: text omits Base64; out.image keeps pixels.
532
+ const imagePayload = names.includes('mimeType') &&
533
+ typeof item.mimeType === 'string' &&
534
+ item.mimeType.startsWith('image/');
535
+ for (const name of names) {
531
536
  if (!first)
532
537
  emit(',');
533
538
  first = false;
534
539
  chunks(name, true);
535
540
  emit(':');
536
- write(item[name], name);
541
+ const field = item[name];
542
+ write(imagePayload &&
543
+ (name === 'dataBase64' || (name === 'data' && item.type === 'image')) &&
544
+ typeof field === 'string'
545
+ ? '[image payload omitted; use out.image]'
546
+ : field, name);
537
547
  }
538
548
  emit('}');
539
549
  }
@@ -30,7 +30,7 @@ Tool calls:
30
30
  - skill, tool_search, question, code_exec, code_exec_wait and direct-only tools cannot be called here. Load Skills directly and read their instructions before the next tool call. Use direct tool_search before a later code cell for deferred tools; the current cell's tools do not change.
31
31
 
32
32
  Built-in namespaces:
33
- - out.text(value): void emits text. Strings and scalar values are stringified; JSON-compatible objects/arrays are serialized as compact JSON. Excess ordinary text is truncated with omission metadata, without stopping later code or tools. Cyclic or unsupported object values fail. Only explicitly emitted output reaches the model.
33
+ - out.text(value): void emits text. Strings and scalar values are stringified; JSON-compatible objects/arrays are serialized as compact JSON. Structured image objects (image/* mimeType and string dataBase64, or native MCP type:image with string data) are recursively summarized without their Base64 payload; originals remain unchanged. Ordinary strings remain unchanged. This does not emit pixels: use out.image(result.images[0]) for images. Prefer result.data or result.content over the whole image result. Excess ordinary text is truncated with omission metadata, without stopping later code or tools. Cyclic or unsupported object values fail. Only explicitly emitted output reaches the model.
34
34
  - out.image(value: {mimeType:string,dataBase64:string}): void emits one returned image; for example out.image(result.images[0]) when that image exists and the model supports images. It does not accept a path, URL, or result.content.
35
35
  - state.get(key), state.has(key), state.set(key, value), state.delete(key) access bounded session JSON state. Mutations commit atomically only when the cell succeeds; failed or cancelled cells discard them.
36
36
  - catalog.tools() returns a read-only snapshot of the tools callable in this cell, including name, description, inputSchema and inputMode. It does not discover or authorize additional tools.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@myagentroam/agent",
3
- "version": "0.9.140",
3
+ "version": "0.9.141",
4
4
  "description": "Embeddable MAR coding agent SDK and CLI.",
5
5
  "license": "Apache-2.0",
6
6
  "type": "module",