@myagentroam/node 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.
@@ -231,6 +231,7 @@ export class CodexAppServerClient {
231
231
  startThread(cwd, configuration = {}) {
232
232
  return this.request('thread/start', {
233
233
  cwd,
234
+ experimentalRawEvents: true,
234
235
  ...codexThreadConfiguration(cwd, configuration)
235
236
  });
236
237
  }
@@ -8,6 +8,7 @@ import { parseSecretEnvironment } from '../../util/secret-environment.js';
8
8
  import { safeErrorCode } from '../../util/safe-error.js';
9
9
  import { runnerErrorProjection } from '../../util/runner-error.js';
10
10
  import { ResponseLatencyTracker } from '../../service/response-latency-tracker.js';
11
+ import { GenerationTimingTracker } from '../../service/generation-timing-tracker.js';
11
12
  export class ClaudeManagedRunController {
12
13
  runner;
13
14
  host;
@@ -164,12 +165,27 @@ export class ClaudeManagedRunController {
164
165
  try {
165
166
  const gate = new ClaudeChannelGate(sessionId);
166
167
  const responseLatency = new ResponseLatencyTracker();
168
+ const generationTiming = new GenerationTimingTracker();
169
+ const finalOutputTokens = new Map();
170
+ const pendingOutputTokens = new Map();
171
+ const completedMessages = new Set();
167
172
  let responseMessageId;
168
173
  const observeResponse = (id, incremental = false) => {
169
174
  const latencyMs = responseLatency.response(id, incremental);
170
175
  if (latencyMs !== undefined)
171
176
  this.host.emit(runId, 'run.performance', { latencyMs });
172
177
  };
178
+ const settleGeneration = (id, observedAt) => {
179
+ if (id === undefined)
180
+ return;
181
+ generationTiming.end(id, observedAt);
182
+ const outputTokens = finalOutputTokens.get(id);
183
+ if (outputTokens === undefined)
184
+ return;
185
+ const result = generationTiming.settle(id, outputTokens, observedAt);
186
+ if (result !== undefined)
187
+ this.host.emit(runId, 'run.performance', { tokensPerSecond: result.tokensPerSecond });
188
+ };
173
189
  handle = this.runner.startChannel({
174
190
  prompt: payload.input,
175
191
  cwd,
@@ -200,9 +216,14 @@ export class ClaudeManagedRunController {
200
216
  }))
201
217
  }),
202
218
  onPermission: (toolName, toolInput, options) => {
203
- const latencyMs = responseLatency.toolStarted(options.toolUseID);
204
- if (latencyMs !== undefined)
205
- this.host.emit(runId, 'run.performance', { latencyMs });
219
+ if (options.agentID === undefined) {
220
+ const receiptAt = generationTiming.timestamp();
221
+ if (responseMessageId !== undefined)
222
+ generationTiming.pause(responseMessageId, receiptAt);
223
+ const latencyMs = responseLatency.toolStarted(options.toolUseID);
224
+ if (latencyMs !== undefined)
225
+ this.host.emit(runId, 'run.performance', { latencyMs });
226
+ }
206
227
  return toolName === 'AskUserQuestion'
207
228
  ? (toolKinds.set(options.toolUseID, 'user_input_request'),
208
229
  this.host.requestUserInput(runId, toolInput, options.toolUseID))
@@ -212,6 +233,7 @@ export class ClaudeManagedRunController {
212
233
  onMessage: (message) => {
213
234
  if (terminal)
214
235
  return;
236
+ const receiptAt = generationTiming.timestamp();
215
237
  const record = message;
216
238
  const nativeSessionId = typeof record.session_id === 'string' ? record.session_id : undefined;
217
239
  const current = this.runner.execution.runs.get(runId);
@@ -236,7 +258,8 @@ export class ClaudeManagedRunController {
236
258
  });
237
259
  }
238
260
  }
239
- if (record.type === 'stream_event') {
261
+ const mainTask = record.parent_tool_use_id === undefined || record.parent_tool_use_id === null;
262
+ if (record.type === 'stream_event' && mainTask) {
240
263
  // Observe semantic timing without forwarding raw thinking or input fragments.
241
264
  const event = record.event;
242
265
  const block = event?.content_block;
@@ -245,14 +268,49 @@ export class ClaudeManagedRunController {
245
268
  const message = event.message;
246
269
  responseMessageId = typeof message?.id === 'string' ? message.id : undefined;
247
270
  }
271
+ if (event?.type === 'message_delta') {
272
+ const usage = event.usage;
273
+ if (responseMessageId !== undefined) {
274
+ if (typeof usage?.output_tokens === 'number')
275
+ pendingOutputTokens.set(responseMessageId, usage.output_tokens);
276
+ if (typeof delta?.stop_reason === 'string' && delta.stop_reason.length > 0) {
277
+ const tokens = pendingOutputTokens.get(responseMessageId);
278
+ if (tokens !== undefined)
279
+ finalOutputTokens.set(responseMessageId, tokens);
280
+ completedMessages.add(responseMessageId);
281
+ settleGeneration(responseMessageId, receiptAt);
282
+ }
283
+ }
284
+ }
285
+ if (event?.type === 'message_stop') {
286
+ if (responseMessageId !== undefined) {
287
+ completedMessages.add(responseMessageId);
288
+ if (!finalOutputTokens.has(responseMessageId)) {
289
+ const tokens = pendingOutputTokens.get(responseMessageId);
290
+ if (tokens !== undefined)
291
+ finalOutputTokens.set(responseMessageId, tokens);
292
+ }
293
+ }
294
+ settleGeneration(responseMessageId, receiptAt);
295
+ }
248
296
  if (event?.type === 'content_block_start' &&
249
- block?.type === 'tool_use' &&
250
- typeof block.id === 'string') {
251
- const latencyMs = responseLatency.toolStarted(block.id);
252
- if (latencyMs !== undefined)
253
- this.host.emit(runId, 'run.performance', { latencyMs });
254
- // A later full snapshot of this streamed message is not a new response.
255
- responseLatency.response(responseMessageId);
297
+ (block?.type === 'tool_use' ||
298
+ block?.type === 'thinking' ||
299
+ block?.type === 'redacted_thinking' ||
300
+ block?.type === 'encrypted_thinking' ||
301
+ block?.type === 'text')) {
302
+ if (responseMessageId !== undefined)
303
+ generationTiming.start(responseMessageId, receiptAt);
304
+ if (block?.type !== 'text' ||
305
+ (typeof block.text === 'string' && block.text.length > 0)) {
306
+ const latencyMs = block?.type === 'tool_use' && typeof block.id === 'string'
307
+ ? responseLatency.toolStarted(block.id)
308
+ : responseLatency.response(responseMessageId);
309
+ if (latencyMs !== undefined)
310
+ this.host.emit(runId, 'run.performance', { latencyMs });
311
+ if (block?.type === 'tool_use')
312
+ responseLatency.response(responseMessageId);
313
+ }
256
314
  }
257
315
  if ((event?.type === 'content_block_start' && block?.type === 'thinking') ||
258
316
  (event?.type === 'content_block_delta' &&
@@ -264,12 +322,27 @@ export class ClaudeManagedRunController {
264
322
  delta.thinking.length > 0) ||
265
323
  (delta?.type === 'input_json_delta' &&
266
324
  typeof delta.partial_json === 'string' &&
267
- delta.partial_json.length > 0))))
325
+ delta.partial_json.length > 0)))) {
326
+ if (responseMessageId !== undefined)
327
+ generationTiming.start(responseMessageId, receiptAt);
268
328
  observeResponse(responseMessageId, event?.type === 'content_block_delta');
329
+ }
269
330
  }
270
331
  if (record.type === 'assistant') {
332
+ const snapshotMessage = record.message;
333
+ const snapshotId = typeof snapshotMessage?.id === 'string' ? snapshotMessage.id : undefined;
334
+ const snapshotUsage = snapshotMessage?.usage;
335
+ if (mainTask &&
336
+ snapshotId !== undefined &&
337
+ typeof snapshotUsage?.output_tokens === 'number' &&
338
+ (completedMessages.has(snapshotId) ||
339
+ (typeof snapshotMessage?.stop_reason === 'string' &&
340
+ snapshotMessage.stop_reason.length > 0))) {
341
+ finalOutputTokens.set(snapshotId, snapshotUsage.output_tokens);
342
+ settleGeneration(snapshotId, receiptAt);
343
+ }
271
344
  const text = extractClaudeText(record);
272
- if (text.length > 0 || extractClaudeThinkingSummaries(record).length > 0)
345
+ if (mainTask && (text.length > 0 || extractClaudeThinkingSummaries(record).length > 0))
273
346
  observeResponse(typeof record.message?.id === 'string'
274
347
  ? record.message.id
275
348
  : undefined);
@@ -292,7 +365,7 @@ export class ClaudeManagedRunController {
292
365
  });
293
366
  }
294
367
  for (const tool of extractClaudeToolUses(record)) {
295
- const latencyMs = responseLatency.toolStarted(tool.id);
368
+ const latencyMs = mainTask ? responseLatency.toolStarted(tool.id) : undefined;
296
369
  if (latencyMs !== undefined)
297
370
  this.host.emit(runId, 'run.performance', { latencyMs });
298
371
  if (tool.name === 'AskUserQuestion') {
@@ -358,7 +431,8 @@ export class ClaudeManagedRunController {
358
431
  }
359
432
  if (record.type === 'user') {
360
433
  for (const result of extractClaudeToolResults(record)) {
361
- responseLatency.toolCompleted(result.toolUseId);
434
+ if (mainTask)
435
+ responseLatency.toolCompleted(result.toolUseId);
362
436
  const kind = toolKinds.get(result.toolUseId) ?? 'tool_call';
363
437
  if (kind === 'user_input_request')
364
438
  continue;
@@ -4,6 +4,7 @@ import { parseSecretEnvironment } from '../../util/secret-environment.js';
4
4
  import { safeErrorCode } from '../../util/safe-error.js';
5
5
  import { runnerErrorProjection } from '../../util/runner-error.js';
6
6
  import { ResponseLatencyTracker } from '../../service/response-latency-tracker.js';
7
+ import { GenerationTimingTracker } from '../../service/generation-timing-tracker.js';
7
8
  import { boundedConversationItemId, codexImageAttachments, codexLiveConversationItem, codexUserInputQuestions, compactRunnerText, isPlainRecord } from './conversation-parser.js';
8
9
  export function resolveCodexNotificationRun(runs, input) {
9
10
  const entries = [...runs.entries()];
@@ -137,6 +138,7 @@ export class CodexManagedRunController {
137
138
  if (run === undefined)
138
139
  return;
139
140
  const [runId, activeRun] = run;
141
+ const receiptAt = activeRun.generationTiming?.timestamp();
140
142
  if (notification.method === 'turn/started' && turnId !== undefined) {
141
143
  const active = this.runner.execution.runs.get(runId);
142
144
  if (active !== undefined)
@@ -144,6 +146,16 @@ export class CodexManagedRunController {
144
146
  this.host.emit(runId, 'run.running', { threadId, turnId }, 'RUNNING');
145
147
  return;
146
148
  }
149
+ if (notification.method === 'rawResponseItem/completed')
150
+ return;
151
+ if (notification.method === 'rawResponse/completed') {
152
+ this.completeRawResponse(runId, params, activeRun, receiptAt);
153
+ return;
154
+ }
155
+ if (notification.method === 'thread/tokenUsage/updated') {
156
+ this.completeStandardUsage(runId, params, activeRun, receiptAt);
157
+ return;
158
+ }
147
159
  if (notification.method === 'thread/goal/updated' &&
148
160
  activeRun.goalMcp === true &&
149
161
  turnId !== undefined &&
@@ -168,6 +180,7 @@ export class CodexManagedRunController {
168
180
  if (notification.requestId !== undefined &&
169
181
  (notification.method === 'item/commandExecution/requestApproval' ||
170
182
  notification.method === 'item/fileChange/requestApproval')) {
183
+ this.pauseGeneration(runId, receiptAt);
171
184
  this.requestApproval(runId, {
172
185
  ...notification,
173
186
  requestId: notification.requestId
@@ -176,6 +189,7 @@ export class CodexManagedRunController {
176
189
  }
177
190
  if (notification.requestId !== undefined &&
178
191
  notification.method === 'item/tool/requestUserInput') {
192
+ this.pauseGeneration(runId, receiptAt);
179
193
  this.requestUserInput(runId, notification.requestId, params);
180
194
  return;
181
195
  }
@@ -200,15 +214,19 @@ export class CodexManagedRunController {
200
214
  text: params.delta,
201
215
  ...(itemId === undefined ? {} : { itemId })
202
216
  });
217
+ if (params.delta.length > 0)
218
+ this.beginGeneration(runId, receiptAt);
203
219
  if (params.delta.length > 0)
204
220
  this.observeResponse(runId, itemId, true);
205
221
  return;
206
222
  }
207
223
  if (notification.method === 'item/reasoning/textDelta' && typeof params.delta === 'string') {
208
- if (params.delta.length > 0)
224
+ if (params.delta.length > 0) {
225
+ this.beginGeneration(runId, receiptAt);
209
226
  this.observeResponse(runId, typeof params.itemId === 'string'
210
227
  ? boundedConversationItemId('codex', params.itemId)
211
228
  : undefined, true);
229
+ }
212
230
  return;
213
231
  }
214
232
  if (notification.method === 'item/reasoning/summaryTextDelta' &&
@@ -225,8 +243,10 @@ export class CodexManagedRunController {
225
243
  merge: true,
226
244
  append: 'sections'
227
245
  });
228
- if (params.delta.length > 0)
246
+ if (params.delta.length > 0) {
247
+ this.beginGeneration(runId, receiptAt);
229
248
  this.observeResponse(runId, boundedConversationItemId('codex', params.itemId), true);
249
+ }
230
250
  return;
231
251
  }
232
252
  if (notification.method === 'item/commandExecution/outputDelta' &&
@@ -260,9 +280,25 @@ export class CodexManagedRunController {
260
280
  const item = codexLiveConversationItem(params.item, notification.method === 'item/completed', attachments);
261
281
  if (item !== undefined) {
262
282
  this.host.emitItem(runId, item);
283
+ if (item.kind === 'context_compaction') {
284
+ activeRun.generationCompacting = notification.method === 'item/started';
285
+ if (activeRun.generationCompacting) {
286
+ for (const key of activeRun.generationKeys ?? [])
287
+ activeRun.generationTiming?.discard(key);
288
+ activeRun.generationKeys = [];
289
+ activeRun.currentGenerationKey = undefined;
290
+ }
291
+ return;
292
+ }
263
293
  const tracker = activeRun.responseLatency;
264
294
  const tool = ['tool_call', 'command_execution', 'file_change'].includes(item.kind);
265
295
  if (tool) {
296
+ if (notification.method === 'item/started') {
297
+ if (codexToolGeneratesArguments(params.item))
298
+ this.beginGeneration(runId, receiptAt);
299
+ else
300
+ this.pauseGeneration(runId, receiptAt);
301
+ }
266
302
  if (notification.method === 'item/completed')
267
303
  tracker?.toolCompleted(item.itemId);
268
304
  else {
@@ -273,12 +309,15 @@ export class CodexManagedRunController {
273
309
  }
274
310
  else if (['assistant_message', 'reasoning_summary', 'plan'].includes(item.kind)) {
275
311
  // A started reasoning item is itself a semantic response, even before its summary.
312
+ if (notification.method === 'item/started')
313
+ this.beginGeneration(runId, receiptAt);
276
314
  this.observeResponse(runId, item.itemId);
277
315
  }
278
316
  }
279
317
  return;
280
318
  }
281
319
  if (notification.method === 'turn/completed') {
320
+ this.endPendingGenerations(activeRun, receiptAt);
282
321
  const status = turn?.status;
283
322
  const terminal = status === 'completed' ? 'SUCCEEDED' : status === 'interrupted' ? 'INTERRUPTED' : 'FAILED';
284
323
  if (terminal === 'FAILED')
@@ -308,6 +347,87 @@ export class CodexManagedRunController {
308
347
  if (latencyMs !== undefined)
309
348
  this.host.emit(runId, 'run.performance', { latencyMs });
310
349
  }
350
+ beginGeneration(runId, observedAt) {
351
+ const active = this.runner.execution.runs.get(runId);
352
+ if (active?.generationTiming === undefined || active.generationCompacting === true)
353
+ return;
354
+ let key = active.currentGenerationKey;
355
+ if (key === undefined) {
356
+ const sequence = (active.generationRequestSequence ?? 0) + 1;
357
+ key = `${active.turnId ?? active.threadId}:generation:${sequence}`;
358
+ active.generationKeys ??= [];
359
+ active.generationKeys.push(key);
360
+ active.generationRequestSequence = sequence;
361
+ active.currentGenerationKey = key;
362
+ }
363
+ active.generationTiming.start(key, observedAt);
364
+ }
365
+ pauseGeneration(runId, observedAt) {
366
+ const active = this.runner.execution.runs.get(runId);
367
+ const key = active?.currentGenerationKey;
368
+ if (active?.generationTiming === undefined || key === undefined)
369
+ return;
370
+ active.generationTiming.pause(key, observedAt);
371
+ }
372
+ endPendingGenerations(active, observedAt) {
373
+ if (active.generationTiming === undefined)
374
+ return;
375
+ for (const key of active.generationKeys ?? [])
376
+ active.generationTiming.end(key, observedAt);
377
+ }
378
+ completeRawResponse(runId, params, active, observedAt) {
379
+ const responseId = typeof params.responseId === 'string' ? params.responseId : undefined;
380
+ if (responseId === undefined || active.completedGenerationResponseIds?.has(responseId))
381
+ return;
382
+ active.completedGenerationResponseIds ??= new Set();
383
+ active.completedGenerationResponseIds.add(responseId);
384
+ if (active.generationCompacting === true)
385
+ return;
386
+ const key = active.generationResponseIds?.get(responseId) ??
387
+ active.currentGenerationKey ??
388
+ active.generationKeys?.find((candidate) => active.generationTiming?.has(candidate));
389
+ if (key === undefined || active.generationTiming === undefined)
390
+ return;
391
+ active.generationResponseIds ??= new Map();
392
+ active.generationResponseIds.set(responseId, key);
393
+ active.generationTiming.end(key, observedAt);
394
+ if (active.currentGenerationKey === key)
395
+ active.currentGenerationKey = undefined;
396
+ const outputTokens = codexOutputTokens(params.usage);
397
+ if (outputTokens === undefined)
398
+ return;
399
+ this.emitGenerationTps(runId, active, key, outputTokens, observedAt);
400
+ }
401
+ completeStandardUsage(runId, params, active, observedAt) {
402
+ if (active.generationTiming === undefined)
403
+ return;
404
+ const usage = isPlainRecord(params.tokenUsage) ? params.tokenUsage : undefined;
405
+ const outputTokens = codexOutputTokens(usage?.last);
406
+ if (outputTokens === undefined)
407
+ return;
408
+ // TokenCount also repeats the previous request's snapshot (for example after compaction).
409
+ // Include total counters to distinguish equal last counts from distinct model requests.
410
+ const snapshot = JSON.stringify([
411
+ codexUsageCounters(usage?.last),
412
+ codexUsageCounters(usage?.total)
413
+ ]);
414
+ if (active.generationUsageSnapshot === snapshot)
415
+ return;
416
+ active.generationUsageSnapshot = snapshot;
417
+ if (active.generationCompacting === true)
418
+ return;
419
+ const key = active.generationKeys?.find((candidate) => active.generationTiming?.has(candidate));
420
+ if (key !== undefined)
421
+ this.emitGenerationTps(runId, active, key, outputTokens, observedAt);
422
+ }
423
+ emitGenerationTps(runId, active, key, outputTokens, observedAt) {
424
+ const result = active.generationTiming?.settle(key, outputTokens, observedAt);
425
+ if (result !== undefined)
426
+ this.host.emit(runId, 'run.performance', { tokensPerSecond: result.tokensPerSecond });
427
+ active.generationKeys = active.generationKeys?.filter((candidate) => candidate !== key);
428
+ if (active.currentGenerationKey === key)
429
+ active.currentGenerationKey = undefined;
430
+ }
311
431
  requestApproval(runId, notification) {
312
432
  const approvalId = `codex:${runId}:${notification.requestId}`;
313
433
  const expiresAt = Date.now() + 10 * 60_000;
@@ -494,7 +614,11 @@ export class CodexManagedRunController {
494
614
  cwd,
495
615
  goalMcp: hasGoalMcp(payload.mcpServers),
496
616
  scheduleMcp: hasScheduleMcp(payload.mcpServers),
497
- responseLatency: new ResponseLatencyTracker()
617
+ responseLatency: new ResponseLatencyTracker(),
618
+ generationTiming: new GenerationTimingTracker(),
619
+ generationKeys: [],
620
+ generationResponseIds: new Map(),
621
+ completedGenerationResponseIds: new Set()
498
622
  });
499
623
  turnResult = await this.runner.startTurn(threadId, input, cwd, turnConfiguration, materializedAttachments, runId);
500
624
  break;
@@ -560,3 +684,25 @@ function hasGoalMcp(value) {
560
684
  function hasScheduleMcp(value) {
561
685
  return isPlainRecord(value) && isPlainRecord(value.mar_schedule);
562
686
  }
687
+ function codexOutputTokens(value) {
688
+ if (!isPlainRecord(value) ||
689
+ typeof value.outputTokens !== 'number' ||
690
+ !Number.isSafeInteger(value.outputTokens) ||
691
+ value.outputTokens < 0)
692
+ return undefined;
693
+ return value.outputTokens;
694
+ }
695
+ function codexToolGeneratesArguments(value) {
696
+ return ['functionCall', 'function_call', 'customToolCall', 'custom_tool_call'].includes(typeof value.type === 'string' ? value.type : '');
697
+ }
698
+ function codexUsageCounters(value) {
699
+ if (!isPlainRecord(value))
700
+ return undefined;
701
+ return [
702
+ value.inputTokens,
703
+ value.cachedInputTokens,
704
+ value.outputTokens,
705
+ value.reasoningOutputTokens,
706
+ value.totalTokens
707
+ ];
708
+ }
@@ -832,7 +832,7 @@ export class MarAgentManagedRunController {
832
832
  this.host.refreshContext?.(runId);
833
833
  if (event.type === 'model.performance')
834
834
  this.host.emit(runId, 'run.performance', {
835
- latencyMs: event.latencyMs,
835
+ ...(event.latencyMs === undefined ? {} : { latencyMs: event.latencyMs }),
836
836
  ...(event.tokensPerSecond === undefined ? {} : { tokensPerSecond: event.tokensPerSecond })
837
837
  });
838
838
  else if (event.type === 'message.delta')
@@ -5,6 +5,7 @@ import { boundedConversationItemId, compactRunnerText } from '../codex/conversat
5
5
  import { openCodePatchToolPayload, openCodeQuestions } from './conversation-parser.js';
6
6
  import { normalizeOpenCodeTool } from './tool-normalizer.js';
7
7
  import { ResponseLatencyTracker } from '../../service/response-latency-tracker.js';
8
+ import { GenerationTimingTracker } from '../../service/generation-timing-tracker.js';
8
9
  export class OpenCodeManagedRunController {
9
10
  runner;
10
11
  host;
@@ -272,6 +273,7 @@ export class OpenCodeManagedRunController {
272
273
  execution.responsePartKinds = new Map();
273
274
  execution.responseStartedAt = Date.now();
274
275
  execution.responseMessages = new Set();
276
+ execution.generationTiming = new GenerationTimingTracker();
275
277
  }
276
278
  await client.prompt({
277
279
  directory: input.cwd,
@@ -295,6 +297,8 @@ export class OpenCodeManagedRunController {
295
297
  const event = record(value);
296
298
  const properties = record(event?.properties);
297
299
  const nested = record(properties?.part) ?? record(properties?.info);
300
+ const execution = this.runner.execution.get(runId);
301
+ const receiptAt = execution?.generationTiming?.timestamp();
298
302
  const sessionID = typeof properties?.sessionID === 'string'
299
303
  ? properties.sessionID
300
304
  : typeof nested?.sessionID === 'string'
@@ -325,8 +329,10 @@ export class OpenCodeManagedRunController {
325
329
  if (info.role === 'assistant' &&
326
330
  execution?.responseStartedAt !== undefined &&
327
331
  typeof created === 'number' &&
328
- created >= execution.responseStartedAt)
332
+ created >= execution.responseStartedAt) {
329
333
  execution.responseMessages?.add(info.id);
334
+ execution.currentResponseMessageId = info.id;
335
+ }
330
336
  }
331
337
  if (info?.role === 'assistant' &&
332
338
  typeof info.id === 'string' &&
@@ -358,8 +364,12 @@ export class OpenCodeManagedRunController {
358
364
  typeof properties.delta === 'string' &&
359
365
  properties.delta.length > 0 &&
360
366
  typeof properties.messageID === 'string' &&
361
- this.currentResponseMessage(runId, properties.messageID))
367
+ this.currentResponseMessage(runId, properties.messageID)) {
368
+ const key = this.generationKey(runId, properties.messageID, properties.partID);
369
+ if (key !== undefined)
370
+ execution?.generationTiming?.start(key, receiptAt);
362
371
  this.observeResponse(runId, boundedConversationItemId('opencode', properties.partID), true);
372
+ }
363
373
  return;
364
374
  }
365
375
  if (event?.type === 'message.part.updated') {
@@ -386,36 +396,91 @@ export class OpenCodeManagedRunController {
386
396
  return;
387
397
  if (typeof part.id === 'string' && typeof part.type === 'string')
388
398
  this.runner.execution.get(runId)?.responsePartKinds?.set(part.id, part.type);
399
+ const tracker = this.currentResponseMessage(runId, part.messageID)
400
+ ? execution?.generationTiming
401
+ : undefined;
402
+ if (part.type === 'step-start') {
403
+ if (tracker !== undefined && typeof part.id === 'string')
404
+ this.generationKey(runId, part.messageID, part.id, receiptAt, true);
405
+ return;
406
+ }
407
+ if (part.type === 'step-finish') {
408
+ const key = typeof part.id === 'string'
409
+ ? (execution?.generationPartKeys?.get(part.id) ??
410
+ execution?.generationRequestKeys?.get(part.messageID))
411
+ : undefined;
412
+ if (tracker !== undefined && key !== undefined && typeof part.id === 'string') {
413
+ execution?.generationPartKeys?.set(part.id, key);
414
+ tracker.end(key, receiptAt);
415
+ if (execution?.generationRequestKeys?.get(part.messageID) === key)
416
+ execution.generationRequestKeys.delete(part.messageID);
417
+ const outputTokens = openCodeOutputTokens(part.tokens);
418
+ if (outputTokens !== undefined) {
419
+ const result = tracker.settle(key, outputTokens, receiptAt);
420
+ if (result !== undefined)
421
+ this.host.emit(runId, 'run.performance', { tokensPerSecond: result.tokensPerSecond });
422
+ }
423
+ }
424
+ return;
425
+ }
389
426
  const item = openCodePart(part, this.runner.execution.get(runId)?.agent === 'plan');
390
427
  if (item !== undefined)
391
428
  this.host.emitItem(runId, item);
392
429
  if (typeof part.id === 'string') {
393
430
  const itemId = boundedConversationItemId('opencode', part.id);
394
- const tracker = this.currentResponseMessage(runId, part.messageID)
395
- ? this.runner.execution.get(runId)?.responseLatency
431
+ const responseLatency = this.currentResponseMessage(runId, part.messageID)
432
+ ? execution?.responseLatency
433
+ : undefined;
434
+ const generationTiming = this.currentResponseMessage(runId, part.messageID)
435
+ ? execution?.generationTiming
396
436
  : undefined;
437
+ const generationKey = generationTiming === undefined
438
+ ? undefined
439
+ : this.generationKey(runId, part.messageID, part.id);
397
440
  if (part.type === 'tool') {
398
441
  const state = record(part.state);
399
- if (state?.status === 'completed' || state?.status === 'error')
400
- tracker?.toolCompleted(itemId);
401
- else if (state?.status === 'pending' || state?.status === 'running') {
402
- const latencyMs = tracker?.toolStarted(itemId);
442
+ if (state?.status === 'pending') {
443
+ if (generationKey !== undefined)
444
+ generationTiming?.start(generationKey, receiptAt);
445
+ const latencyMs = responseLatency?.toolStarted(itemId);
446
+ if (latencyMs !== undefined)
447
+ this.host.emit(runId, 'run.performance', { latencyMs });
448
+ }
449
+ else if (state?.status === 'running') {
450
+ if (generationKey !== undefined)
451
+ generationTiming?.pause(generationKey, receiptAt);
452
+ const latencyMs = responseLatency?.toolStarted(itemId);
403
453
  if (latencyMs !== undefined)
404
454
  this.host.emit(runId, 'run.performance', { latencyMs });
405
455
  }
456
+ else if (state?.status === 'completed' || state?.status === 'error') {
457
+ if (generationKey !== undefined)
458
+ generationTiming?.pause(generationKey, receiptAt);
459
+ responseLatency?.toolCompleted(itemId);
460
+ }
406
461
  }
407
- else if (tracker !== undefined &&
462
+ else if (generationTiming !== undefined &&
463
+ generationKey !== undefined &&
408
464
  (part.type === 'text' || part.type === 'reasoning') &&
409
- typeof part.text === 'string' &&
410
- part.text.length > 0)
411
- this.observeResponse(runId, itemId);
465
+ typeof part.text === 'string') {
466
+ if (isOpenCodeGenerationStart(part.time))
467
+ generationTiming.start(generationKey, receiptAt);
468
+ if (isOpenCodeGenerationEnd(part.time))
469
+ generationTiming.pause(generationKey, receiptAt);
470
+ if (part.text.length > 0)
471
+ this.observeResponse(runId, itemId);
472
+ }
412
473
  }
413
474
  return;
414
475
  }
415
- if (event?.type === 'permission.asked')
476
+ if (event?.type === 'permission.asked') {
477
+ this.pauseCurrentGeneration(runId, receiptAt);
416
478
  this.requestPermission(runId, properties);
417
- if (event?.type === 'question.asked')
479
+ }
480
+ if (event?.type === 'question.asked') {
481
+ this.pauseCurrentGeneration(runId, receiptAt);
418
482
  this.requestQuestion(runId, properties);
483
+ }
419
484
  }
420
485
  currentResponseMessage(runId, messageId) {
421
486
  const parent = this.messageParents.get(runId)?.get(messageId);
@@ -430,6 +495,37 @@ export class OpenCodeManagedRunController {
430
495
  if (latencyMs !== undefined)
431
496
  this.host.emit(runId, 'run.performance', { latencyMs });
432
497
  }
498
+ pauseCurrentGeneration(runId, observedAt) {
499
+ const execution = this.runner.execution.get(runId);
500
+ const key = execution?.currentResponseMessageId === undefined
501
+ ? undefined
502
+ : execution.generationRequestKeys?.get(execution.currentResponseMessageId);
503
+ if (key !== undefined)
504
+ execution?.generationTiming?.pause(key, observedAt);
505
+ }
506
+ generationKey(runId, messageId, partId, observedAt, newRequest = false) {
507
+ const execution = this.runner.execution.get(runId);
508
+ if (execution?.generationTiming === undefined)
509
+ return undefined;
510
+ execution.generationPartKeys ??= new Map();
511
+ const existing = execution.generationPartKeys.get(partId);
512
+ if (existing !== undefined)
513
+ return existing;
514
+ execution.generationRequestKeys ??= new Map();
515
+ let key = execution.generationRequestKeys.get(messageId);
516
+ if (newRequest && key !== undefined) {
517
+ execution.generationTiming.end(key, observedAt);
518
+ key = undefined;
519
+ }
520
+ if (key === undefined) {
521
+ const sequence = (execution.generationRequestSequence ?? 0) + 1;
522
+ execution.generationRequestSequence = sequence;
523
+ key = `${messageId}:generation:${sequence}`;
524
+ execution.generationRequestKeys.set(messageId, key);
525
+ }
526
+ execution.generationPartKeys.set(partId, key);
527
+ return key;
528
+ }
433
529
  requestPermission(runId, properties) {
434
530
  const execution = this.runner.execution.get(runId);
435
531
  const requestId = typeof properties?.id === 'string' ? properties.id : undefined;
@@ -655,3 +751,21 @@ function record(value) {
655
751
  ? value
656
752
  : undefined;
657
753
  }
754
+ function isOpenCodeGenerationStart(value) {
755
+ return typeof record(value)?.start === 'number';
756
+ }
757
+ function isOpenCodeGenerationEnd(value) {
758
+ return typeof record(value)?.end === 'number';
759
+ }
760
+ function openCodeOutputTokens(value) {
761
+ const tokens = record(value);
762
+ const output = tokens?.output;
763
+ const reasoning = tokens?.reasoning;
764
+ if (!Number.isSafeInteger(output) ||
765
+ !Number.isSafeInteger(reasoning) ||
766
+ output < 0 ||
767
+ reasoning < 0 ||
768
+ output + reasoning > Number.MAX_SAFE_INTEGER)
769
+ return undefined;
770
+ return output + reasoning;
771
+ }
@@ -0,0 +1,81 @@
1
+ export class GenerationTimingTracker {
2
+ now;
3
+ requests = new Map();
4
+ constructor(now = () => performance.now()) {
5
+ this.now = now;
6
+ }
7
+ timestamp() {
8
+ return this.now();
9
+ }
10
+ start(key, observedAt) {
11
+ const at = observedAt ?? this.now();
12
+ const request = this.requests.get(key) ?? {
13
+ started: false,
14
+ durationMs: 0,
15
+ ended: false,
16
+ settled: false,
17
+ invalid: false
18
+ };
19
+ this.observe(request, at);
20
+ if (request.ended || request.settled || request.invalid || request.runningAt !== undefined)
21
+ return false;
22
+ request.started = true;
23
+ request.runningAt = at;
24
+ this.requests.set(key, request);
25
+ return true;
26
+ }
27
+ pause(key, observedAt) {
28
+ const request = this.requests.get(key);
29
+ if (request === undefined || request.settled)
30
+ return;
31
+ const at = observedAt ?? this.now();
32
+ this.observe(request, at);
33
+ if (request.runningAt === undefined)
34
+ return;
35
+ const duration = at - request.runningAt;
36
+ if (!(duration >= 0))
37
+ request.invalid = true;
38
+ else
39
+ request.durationMs += duration;
40
+ request.runningAt = undefined;
41
+ }
42
+ end(key, observedAt) {
43
+ const request = this.requests.get(key);
44
+ if (request === undefined || request.settled || request.ended)
45
+ return;
46
+ this.pause(key, observedAt);
47
+ request.ended = true;
48
+ }
49
+ settle(key, outputTokens, observedAt) {
50
+ const request = this.requests.get(key);
51
+ if (request === undefined ||
52
+ request.settled ||
53
+ !request.started ||
54
+ request.invalid ||
55
+ !Number.isSafeInteger(outputTokens) ||
56
+ outputTokens < 0)
57
+ return undefined;
58
+ this.end(key, observedAt);
59
+ request.settled = true;
60
+ if (request.invalid || request.durationMs < 1)
61
+ return undefined;
62
+ return {
63
+ outputTokens,
64
+ durationMs: request.durationMs,
65
+ tokensPerSecond: Math.round((outputTokens * 100_000) / request.durationMs) / 100
66
+ };
67
+ }
68
+ discard(key) {
69
+ this.requests.delete(key);
70
+ }
71
+ has(key) {
72
+ return this.requests.has(key);
73
+ }
74
+ observe(request, observedAt) {
75
+ if (!Number.isFinite(observedAt) ||
76
+ (request.lastAt !== undefined && observedAt < request.lastAt))
77
+ request.invalid = true;
78
+ if (Number.isFinite(observedAt))
79
+ request.lastAt = observedAt;
80
+ }
81
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@myagentroam/node",
3
- "version": "0.9.174",
3
+ "version": "0.9.175",
4
4
  "description": "MyAgentRoam Node runtime CLI.",
5
5
  "license": "Apache-2.0 AND GPL-2.0-or-later",
6
6
  "type": "module",
@@ -36,10 +36,10 @@
36
36
  "playwright": "1.62.1",
37
37
  "ws": "^8.21.3",
38
38
  "zod": "4.4.3",
39
- "@myagentroam/agent": "0.9.174",
40
- "@myagentroam/protocol": "0.9.174",
41
- "@myagentroam/code-navigation": "0.9.129",
42
- "@myagentroam/marctl": "0.9.123"
39
+ "@myagentroam/agent": "0.9.175",
40
+ "@myagentroam/marctl": "0.9.123",
41
+ "@myagentroam/protocol": "0.9.175",
42
+ "@myagentroam/code-navigation": "0.9.129"
43
43
  },
44
44
  "devDependencies": {
45
45
  "@types/ws": "^8.18.1"