@myagentroam/node 0.10.7 → 0.10.9

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.
Files changed (34) hide show
  1. package/dist/capabilities.js +1 -0
  2. package/dist/connector.js +60 -5
  3. package/dist/database.js +3 -1
  4. package/dist/migrations/v017.js +46 -0
  5. package/dist/native-session-history.js +7 -15
  6. package/dist/runner/abstract-runner.js +4 -0
  7. package/dist/runner/claude/managed-run-controller.js +15 -3
  8. package/dist/runner/claude/tool-normalizer.js +100 -6
  9. package/dist/runner/codex/conversation-parser.js +41 -30
  10. package/dist/runner/codex/managed-run-controller.js +12 -1
  11. package/dist/runner/codex/tool-normalizer.js +5 -2
  12. package/dist/runner/mar-agent/compact-extensions.js +1 -0
  13. package/dist/runner/mar-agent/conversation-normalizer.js +43 -78
  14. package/dist/runner/mar-agent/hosted-conversation-runtime.js +27 -0
  15. package/dist/runner/mar-agent/managed-run-controller.js +13 -1
  16. package/dist/runner/mar-agent/runtime-model-factory.js +5 -0
  17. package/dist/runner/mar-agent-runner.js +15 -0
  18. package/dist/runner/opencode/conversation-parser.js +49 -0
  19. package/dist/runner/opencode/managed-run-controller.js +12 -0
  20. package/dist/service/continuous-run-coordinator.js +1 -0
  21. package/dist/service/hosted-conversation-node-service.js +29 -2
  22. package/dist/service/mar-agent-model-transport-test-service.js +16 -1
  23. package/dist/service/model-usage-outbox.js +167 -0
  24. package/dist/service/model-usage-service.js +61 -0
  25. package/dist/service/node-control-message-service.js +7 -2
  26. package/dist/service/node-request-service.js +3 -1
  27. package/dist/service/node-run-event-bridge.js +6 -0
  28. package/dist/service/runner-usage-tracker.js +62 -0
  29. package/dist/service/session-command-service.js +4 -0
  30. package/dist/service/session-message-service.js +6 -0
  31. package/dist/service/session-query-service.js +16 -0
  32. package/dist/util/node-operation-parsers.js +0 -21
  33. package/dist/util/runner-native-session-parsers.js +9 -4
  34. package/package.json +4 -4
@@ -5,7 +5,7 @@ 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
7
  import { GenerationTimingTracker } from '../../service/generation-timing-tracker.js';
8
- import { boundedConversationItemId, codexImageAttachments, codexLiveConversationItem, codexUserInputQuestions, compactRunnerText, isPlainRecord } from './conversation-parser.js';
8
+ import { boundedConversationItemId, codexImageAttachments, codexLiveConversationItem, CodexUsageNormalizer, codexUserInputQuestions, compactRunnerText, isPlainRecord } from './conversation-parser.js';
9
9
  export function resolveCodexNotificationRun(runs, input) {
10
10
  const entries = [...runs.entries()];
11
11
  if (input.turnId !== undefined) {
@@ -28,6 +28,7 @@ export function resolveCodexNotificationRun(runs, input) {
28
28
  export class CodexManagedRunController {
29
29
  runner;
30
30
  host;
31
+ usage = new Map();
31
32
  constructor(runner, host) {
32
33
  this.runner = runner;
33
34
  this.host = host;
@@ -113,6 +114,7 @@ export class CodexManagedRunController {
113
114
  catch (error) {
114
115
  this.emitError(runId, error, 'CODEX_COMPACT_FAILED');
115
116
  this.host.emit(runId, 'run.failed', runnerErrorProjection(error, 'CODEX_COMPACT_FAILED'), 'FAILED');
117
+ this.usage.delete(runId);
116
118
  this.runner.execution.runs.delete(runId);
117
119
  this.runner.releaseEnvironment(runId);
118
120
  this.host.removeActive(runId);
@@ -138,6 +140,12 @@ export class CodexManagedRunController {
138
140
  if (run === undefined)
139
141
  return;
140
142
  const [runId, activeRun] = run;
143
+ let usage = this.usage.get(runId);
144
+ if (!usage) {
145
+ usage = new CodexUsageNormalizer(runId, (value) => this.host.emit(runId, 'run.usage', value), this.host.session(activeRun.sessionId)?.model ?? undefined);
146
+ this.usage.set(runId, usage);
147
+ }
148
+ usage.observe(notification.method, params, activeRun.generationCompacting === true);
141
149
  const receiptAt = activeRun.generationTiming?.timestamp();
142
150
  if (notification.method === 'turn/started' && turnId !== undefined) {
143
151
  const active = this.runner.execution.runs.get(runId);
@@ -332,6 +340,7 @@ export class CodexManagedRunController {
332
340
  this.host.session(active.sessionId)?.titleSource !== 'AUTO'
333
341
  ? undefined
334
342
  : this.host.syncTitle(active.sessionId, active.threadId, active.cwd);
343
+ this.usage.delete(runId);
335
344
  this.runner.execution.runs.delete(runId);
336
345
  if (titleRefresh === undefined)
337
346
  this.runner.releaseEnvironment(runId);
@@ -624,6 +633,7 @@ export class CodexManagedRunController {
624
633
  break;
625
634
  }
626
635
  catch (error) {
636
+ this.usage.delete(runId);
627
637
  this.runner.execution.runs.delete(runId);
628
638
  if (attempt === 0 && (await this.runner.recoverMissingRollout(runId, error)))
629
639
  continue;
@@ -663,6 +673,7 @@ export class CodexManagedRunController {
663
673
  }
664
674
  this.emitError(runId, error, 'CODEX_RUN_FAILED');
665
675
  this.host.emit(runId, 'run.failed', runnerErrorProjection(error, 'CODEX_RUN_FAILED'), 'FAILED');
676
+ this.usage.delete(runId);
666
677
  this.runner.execution.runs.delete(runId);
667
678
  this.runner.releaseEnvironment(runId);
668
679
  this.host.removeActive(runId);
@@ -96,7 +96,10 @@ function codexSubagentProjection(namespace, toolName, input, output) {
96
96
  : '';
97
97
  return {
98
98
  subagentOperation: operation,
99
- subagentId: codexSubagentId(input) ?? codexSubagentId(output) ?? null,
99
+ ...(toolName === 'wait_agent' ? { subagentWait: true } : {}),
100
+ subagentId: Array.isArray(inputData?.targets) && inputData.targets.length > 1
101
+ ? null
102
+ : (codexSubagentId(input) ?? codexSubagentId(output) ?? null),
100
103
  ...(title.length === 0 ? {} : { title })
101
104
  };
102
105
  }
@@ -109,7 +112,7 @@ function codexSubagentId(value) {
109
112
  if (candidate !== undefined)
110
113
  return candidate;
111
114
  }
112
- if (Array.isArray(data.targets)) {
115
+ if (Array.isArray(data.targets) && data.targets.length === 1) {
113
116
  for (const target of data.targets) {
114
117
  const candidate = boundedId(target);
115
118
  if (candidate !== undefined)
@@ -104,6 +104,7 @@ export async function generateMarAgentCompact(context, selected, signal) {
104
104
  try {
105
105
  const result = await generateModelText({
106
106
  model: selected.model,
107
+ ...(context.reportModelUsage === undefined ? {} : { onModelUsage: context.reportModelUsage }),
107
108
  system,
108
109
  reasoningEffort: selected.reasoningEffort,
109
110
  messages: [
@@ -1,3 +1,9 @@
1
+ import { parseToolResultExtensions } from '@myagentroam/agent';
2
+ /** SDK usage is already normalized; never parse provider payloads in Node. */
3
+ export function normalizeMarAgentUsage(usage) {
4
+ const { executionId: _executionId, ...record } = usage;
5
+ return { ...record, schemaVersion: 1, coverage: 'request' };
6
+ }
1
7
  import { boundedConversationItemId, compactRunnerText, compactRunnerValue } from '../../util/node-operation-parsers.js';
2
8
  import { runnerErrorProjection } from '../../util/runner-error.js';
3
9
  export class MarAgentConversationNormalizer {
@@ -6,8 +12,6 @@ export class MarAgentConversationNormalizer {
6
12
  #processOrigins = new Map();
7
13
  #subagentTitles = new Map();
8
14
  #subagentDetails = new Map();
9
- #subagentStarts = new Map();
10
- #subagentLifecycles = new Map();
11
15
  #codeModeObservationIds = new Set();
12
16
  #codeModeSourceCallIds = new Set();
13
17
  messageItemId(nativeItemId) {
@@ -183,9 +187,6 @@ export class MarAgentConversationNormalizer {
183
187
  : undefined;
184
188
  const subagentTitle = agentId === undefined ? undefined : this.#subagentTitles.get(agentId);
185
189
  const subagentDetails = agentId === undefined ? undefined : this.#subagentDetails.get(agentId);
186
- const toolSubagentDetails = event.toolName === 'agent_message'
187
- ? withoutSubagentLatestMessage(subagentDetails)
188
- : subagentDetails;
189
190
  this.#tools.set(event.callId, {
190
191
  toolName: event.toolName,
191
192
  input: event.input,
@@ -197,7 +198,7 @@ export class MarAgentConversationNormalizer {
197
198
  if (event.toolName === 'apply_patch' && 'input' in event)
198
199
  return [fileChangeItem(ownItemId, 'IN_PROGRESS', [])];
199
200
  return [
200
- genericToolItem(ownItemId, event.toolName, 'IN_PROGRESS', event.input, null, null, event.truncated === true, undefined, subagentTitle, toolSubagentDetails)
201
+ genericToolItem(ownItemId, event.toolName, 'IN_PROGRESS', event.input, null, null, event.truncated === true, undefined, subagentTitle, subagentDetails)
201
202
  ];
202
203
  }
203
204
  #toolCompleted(event) {
@@ -220,9 +221,11 @@ export class MarAgentConversationNormalizer {
220
221
  }
221
222
  }
222
223
  if (event.toolName === 'apply_patch' &&
223
- (state?.structured === true || Array.isArray(record(event.data)?.files)))
224
+ (state?.structured === true ||
225
+ Array.isArray(record(event.data)?.files) ||
226
+ parseToolResultExtensions(event.resultExtensions) !== undefined))
224
227
  return [
225
- fileChangeItem(state?.itemId ?? itemId('tool', event.callId), 'COMPLETED', patchChanges(event.data))
228
+ fileChangeItem(state?.itemId ?? itemId('tool', event.callId), 'COMPLETED', patchChanges(event.data, event.resultExtensions))
226
229
  ];
227
230
  const generatedImages = imageArtifacts(event);
228
231
  const controlTool = isMarAgentControlTool(event.toolName);
@@ -236,9 +239,7 @@ export class MarAgentConversationNormalizer {
236
239
  ? undefined
237
240
  : mergeSubagentDetails(this.#subagentDetails.get(agentId), state?.subagentDetails ?? {});
238
241
  const details = controlTool
239
- ? mergeSubagentDetails(event.toolName === 'agent_message'
240
- ? withoutSubagentLatestMessage(cachedDetails)
241
- : cachedDetails, subagentDetails(controlOutput))
242
+ ? mergeSubagentDetails(cachedDetails, subagentDetails(controlOutput))
242
243
  : {};
243
244
  if (agentId !== undefined) {
244
245
  const output = structuredRecord(controlOutput);
@@ -248,18 +249,11 @@ export class MarAgentConversationNormalizer {
248
249
  this.#subagentTitles.set(agentId, title);
249
250
  }
250
251
  if (Object.keys(details).length > 0)
251
- this.#subagentDetails.set(agentId, details);
252
- if (event.toolName === 'agent_start' && state !== undefined)
253
- this.#subagentStarts.set(agentId, state);
252
+ this.#subagentDetails.set(agentId, withoutSubagentLatestMessage(details) ?? {});
254
253
  }
255
- const lifecycle = event.toolName === 'agent_start' && agentId !== undefined
256
- ? this.#subagentLifecycles.get(agentId)
257
- : undefined;
258
- const item = genericToolItem(state?.itemId ?? itemId('tool', event.callId), event.toolName, lifecycle?.status ??
259
- (event.toolName === 'agent_start' ? subagentControlStatus(controlOutput) : 'COMPLETED'), state?.input, lifecycle?.outputSummary ??
260
- (generatedImages.length === 0
261
- ? event.summary
262
- : generatedImages.map((image) => image.name).join(', ')), lifecycle?.errorCode ?? null, lifecycle?.truncated ?? event.truncated, event.data, agentId === undefined ? state?.subagentTitle : this.#subagentTitles.get(agentId), details);
254
+ const item = genericToolItem(state?.itemId ?? itemId('tool', event.callId), event.toolName, 'COMPLETED', state?.input, generatedImages.length === 0
255
+ ? event.summary
256
+ : generatedImages.map((image) => image.name).join(', '), null, event.truncated, event.data, agentId === undefined ? state?.subagentTitle : this.#subagentTitles.get(agentId), details);
263
257
  return [
264
258
  {
265
259
  ...item,
@@ -283,7 +277,9 @@ export class MarAgentConversationNormalizer {
283
277
  }
284
278
  }
285
279
  if (event.toolName === 'apply_patch' && state?.structured === true)
286
- return [fileChangeItem(state?.itemId ?? itemId('tool', event.callId), 'FAILED', [])];
280
+ return [
281
+ fileChangeItem(state?.itemId ?? itemId('tool', event.callId), 'FAILED', patchChanges(event.data, event.resultExtensions))
282
+ ];
287
283
  return [
288
284
  genericToolItem(state?.itemId ?? itemId('tool', event.callId), event.toolName, 'FAILED', state?.input, event.message ?? event.code, event.code, event.truncated === true, undefined, state?.subagentTitle, state?.subagentDetails)
289
285
  ];
@@ -335,6 +331,9 @@ export class MarAgentConversationNormalizer {
335
331
  toolName,
336
332
  callId,
337
333
  summary: observation.result.content,
334
+ ...(parseToolResultExtensions(observation.resultExtensions)
335
+ ? { resultExtensions: parseToolResultExtensions(observation.resultExtensions) }
336
+ : {}),
338
337
  ...(observation.result.truncated === undefined
339
338
  ? {}
340
339
  : { truncated: observation.result.truncated }),
@@ -346,39 +345,10 @@ export class MarAgentConversationNormalizer {
346
345
  const eventTitle = compactRunnerText(typeof eventDescription === 'string' ? eventDescription : '', 500);
347
346
  if (eventTitle.length > 0)
348
347
  this.#subagentTitles.set(event.agentId, eventTitle);
349
- const title = eventTitle || this.#subagentTitles.get(event.agentId) || 'Subagent';
350
- const details = mergeSubagentDetails(this.#subagentDetails.get(event.agentId), subagentDetails(event));
348
+ const details = mergeSubagentDetails(this.#subagentDetails.get(event.agentId), withoutSubagentLatestMessage(subagentDetails(event)) ?? {});
351
349
  if (Object.keys(details).length > 0)
352
350
  this.#subagentDetails.set(event.agentId, details);
353
- const latestText = typeof details.latestMessage?.text === 'string' ? details.latestMessage.text : undefined;
354
- const outputSummary = event.type === 'subagent.completed'
355
- ? latestText === undefined
356
- ? event.errorMessage === undefined
357
- ? null
358
- : boundedToolSummary(event.errorMessage)
359
- : boundedToolSummary(latestText)
360
- : null;
361
- const lifecycle = {
362
- status: event.type === 'subagent.started'
363
- ? 'IN_PROGRESS'
364
- : event.status === 'completed'
365
- ? 'COMPLETED'
366
- : event.status === 'cancelled'
367
- ? 'CANCELLED'
368
- : event.status === 'interrupted'
369
- ? 'INTERRUPTED'
370
- : 'FAILED',
371
- outputSummary,
372
- errorCode: event.type === 'subagent.completed' && event.status === 'failed'
373
- ? (event.errorCode ?? 'MAR_AGENT_SUBAGENT_FAILED')
374
- : null,
375
- truncated: latestText !== undefined && outputSummary !== latestText
376
- };
377
- this.#subagentLifecycles.set(event.agentId, lifecycle);
378
- const start = this.#subagentStarts.get(event.agentId);
379
- if (start === undefined)
380
- return [];
381
- return [subagentStartItem(start, event.agentId, title, details, lifecycle)];
351
+ return [];
382
352
  }
383
353
  }
384
354
  function codeModeToolResults(event) {
@@ -422,6 +392,7 @@ function codeModeCallResults(value) {
422
392
  {
423
393
  source: { callId: call.callId, toolName: call.toolName, input: call.input },
424
394
  outcome: call.status,
395
+ resultExtensions: call.resultExtensions,
425
396
  result: {
426
397
  content: result.content,
427
398
  ...('data' in result ? { data: result.data } : {}),
@@ -582,15 +553,6 @@ function genericToolItem(id, toolName, status, input, outputSummary = null, erro
582
553
  merge: true
583
554
  };
584
555
  }
585
- function subagentStartItem(start, agentId, title, details, lifecycle) {
586
- return genericToolItem(start.itemId, 'agent_start', lifecycle.status, start.input, lifecycle.outputSummary, lifecycle.errorCode, lifecycle.truncated, {
587
- agentId,
588
- description: title,
589
- ...details,
590
- ...(lifecycle.errorCode === null ? {} : { errorCode: lifecycle.errorCode }),
591
- ...(lifecycle.outputSummary === null ? {} : { errorMessage: lifecycle.outputSummary })
592
- }, title, details);
593
- }
594
556
  function commandItem(command, status) {
595
557
  return {
596
558
  itemId: command.itemId,
@@ -623,7 +585,23 @@ function applyCommandResult(command, result) {
623
585
  function fileChangeItem(id, status, changes) {
624
586
  return { itemId: id, kind: 'file_change', status, payload: { changes }, merge: true };
625
587
  }
626
- function patchChanges(value) {
588
+ function patchChanges(value, extensions) {
589
+ const evidence = (parseToolResultExtensions(extensions) ?? []).flatMap((extension) => extension.entries);
590
+ if (evidence.length)
591
+ return evidence.map((entry) => ({
592
+ path: entry.path,
593
+ change: entry.change.toUpperCase(),
594
+ additions: entry.additions,
595
+ deletions: entry.deletions,
596
+ changeId: entry.changeId,
597
+ diffAvailable: entry.diff.status === 'available',
598
+ diff: entry.diff.status === 'available'
599
+ ? { ...entry.diff, source: 'TOOL_CHANGE' }
600
+ : {
601
+ ...entry.diff,
602
+ reason: entry.diff.reason === 'unsupported-content' ? 'binary' : entry.diff.reason
603
+ }
604
+ }));
627
605
  const files = record(value)?.files;
628
606
  if (!Array.isArray(files))
629
607
  return [];
@@ -723,6 +701,7 @@ function marAgentSubagentProjection(toolName, input, output, knownTitle, knownDe
723
701
  : ''), 500);
724
702
  return {
725
703
  subagentOperation: operation,
704
+ ...(operation === 'STATUS' ? { subagentWait: inputData?.waitMs !== 0 } : {}),
726
705
  subagentId: marAgentAgentId(input) ?? marAgentAgentId(controlOutput) ?? null,
727
706
  ...(title.length === 0 ? {} : { title }),
728
707
  ...subagentPayload(details),
@@ -762,20 +741,6 @@ function marAgentSubagentOperation(toolName) {
762
741
  return 'CANCEL';
763
742
  return undefined;
764
743
  }
765
- function subagentControlStatus(value) {
766
- const status = structuredRecord(value)?.status;
767
- if (status === 'queued' || status === 'running')
768
- return 'IN_PROGRESS';
769
- if (status === 'completed')
770
- return 'COMPLETED';
771
- if (status === 'cancelled')
772
- return 'CANCELLED';
773
- if (status === 'interrupted')
774
- return 'INTERRUPTED';
775
- if (status === 'failed')
776
- return 'FAILED';
777
- return 'COMPLETED';
778
- }
779
744
  function subagentDetails(value) {
780
745
  const data = structuredRecord(value);
781
746
  if (data === undefined)
@@ -57,6 +57,19 @@ export class MarAgentHostedConversationRuntime {
57
57
  await mcp?.close().catch(() => undefined);
58
58
  }
59
59
  }
60
+ async readToolResultResource(input) {
61
+ const catalog = await this.#openCatalog({ homeDirectory: input.homeDirectory });
62
+ try {
63
+ return await catalog.readToolResultResource(input.sessionId, {
64
+ resourceRef: input.resourceRef,
65
+ ...(input.cursor === undefined ? {} : { cursor: input.cursor }),
66
+ ...(input.maxBytes === undefined ? {} : { maxBytes: input.maxBytes })
67
+ });
68
+ }
69
+ finally {
70
+ await catalog.close();
71
+ }
72
+ }
60
73
  async history(input) {
61
74
  const catalog = await this.#openCatalog({ homeDirectory: input.homeDirectory });
62
75
  try {
@@ -147,6 +160,17 @@ export class MarAgentHostedConversationRuntime {
147
160
  models: sdkModelConfigurations(models.filter((model) => input.models.some((candidate) => candidate.id === model.id))),
148
161
  currentModelId: input.modelId,
149
162
  prompt: input.userPrompt,
163
+ ...(input.onModelUsage === undefined
164
+ ? {}
165
+ : {
166
+ onModelUsage: (usage) => input.onModelUsage({
167
+ ...usage,
168
+ sessionId: input.sessionId,
169
+ executionId: execution.executionId,
170
+ purpose: 'title',
171
+ agentRole: 'root'
172
+ })
173
+ }),
150
174
  signal: titleController.signal
151
175
  }).catch(() => undefined);
152
176
  await this.#onEvent?.(input.conversationId, event);
@@ -302,6 +326,9 @@ export class MarAgentHostedConversationRuntime {
302
326
  ...(this.#webFetch === undefined ? {} : { webFetch: this.#webFetch })
303
327
  }));
304
328
  host = this.#createHost({
329
+ ...('onModelUsage' in input && input.onModelUsage !== undefined
330
+ ? { modelUsage: input.onModelUsage }
331
+ : {}),
305
332
  extensions,
306
333
  mode: 'node',
307
334
  homeDirectory: input.homeDirectory,
@@ -12,7 +12,7 @@ import { boundedConversationItemId } from '../codex/conversation-parser.js';
12
12
  import { parseSecretEnvironment } from '../../util/secret-environment.js';
13
13
  import { canonicalJson } from '../../util/canonical-json.js';
14
14
  import { MarAgentMcpClientCache } from './mcp-client-pool.js';
15
- import { MarAgentConversationNormalizer } from './conversation-normalizer.js';
15
+ import { MarAgentConversationNormalizer, normalizeMarAgentUsage } from './conversation-normalizer.js';
16
16
  import { parseRunnerImageAttachments } from '../../util/node-operation-parsers.js';
17
17
  import { runnerErrorProjection } from '../../util/runner-error.js';
18
18
  import { MarAgentRuntimeModelFactory, sdkModelConfigurations, closeImageCredentialProviders } from './runtime-model-factory.js';
@@ -435,6 +435,13 @@ export class MarAgentManagedRunController {
435
435
  models: sdkModelConfigurations(executionModels),
436
436
  currentModelId: input.model.id,
437
437
  prompt: input.input,
438
+ onModelUsage: (usage) => this.host.emit(input.runId, 'run.usage', normalizeMarAgentUsage({
439
+ ...usage,
440
+ sessionId: session.id,
441
+ executionId: handle.executionId,
442
+ purpose: 'title',
443
+ agentRole: 'root'
444
+ })),
438
445
  signal: titleController.signal
439
446
  }).catch((error) => {
440
447
  if (!titleController.signal.aborted)
@@ -635,6 +642,11 @@ export class MarAgentManagedRunController {
635
642
  releaseEnvironment = this.#retainEnvironment?.(input.secretEnvironment ?? {});
636
643
  const mcp = mcpLease.pool;
637
644
  const localHost = new LocalAgentHost({
645
+ modelUsage: (usage) => {
646
+ const runId = runtime?.activeRunId;
647
+ if (runId !== undefined)
648
+ this.host.emit(runId, 'run.usage', normalizeMarAgentUsage(usage));
649
+ },
638
650
  mode: 'node',
639
651
  homeDirectory: homedir(),
640
652
  workspace: input.cwd,
@@ -76,6 +76,11 @@ export class MarAgentRuntimeModelFactory {
76
76
  ? {}
77
77
  : { responsesTransport: configuration.responsesTransport }),
78
78
  responsesToolNamespace: configuration.responsesToolNamespace,
79
+ ...(configuration.responsesOptionalParameters === undefined
80
+ ? {}
81
+ : {
82
+ responsesOptionalParameters: configuration.responsesOptionalParameters
83
+ }),
79
84
  responsesPreviousResponseId: configuration.responsesPreviousResponseId,
80
85
  enabled: configuration.enabled
81
86
  };
@@ -71,6 +71,21 @@ export class MarAgentRunner extends AbstractRunner {
71
71
  let registeredIndex = 0;
72
72
  return entry.images.map((image) => image.readable ? registeredImages[registeredIndex++] : undefined);
73
73
  }
74
+ async readToolResultResource(session, input) {
75
+ if (!session.externalSessionId)
76
+ throw new Error('SESSION_NOT_FOUND');
77
+ const catalog = await openMarAgentSessionCatalog({ homeDirectory: this.homeDirectory });
78
+ try {
79
+ return await catalog.readToolResultResource(session.externalSessionId, {
80
+ resourceRef: input.resourceRef,
81
+ ...(input.cursor === undefined ? {} : { cursor: input.cursor }),
82
+ ...(input.maxBytes === undefined ? {} : { maxBytes: input.maxBytes })
83
+ });
84
+ }
85
+ finally {
86
+ await catalog.close();
87
+ }
88
+ }
74
89
  profile(capabilities) {
75
90
  if (!this.available(capabilities))
76
91
  return undefined;
@@ -1,6 +1,55 @@
1
1
  import { boundedConversationItemId, compactRunnerText } from '../codex/conversation-parser.js';
2
2
  import { publicGoalHistoryText } from '../../service/goal-runtime.js';
3
3
  import { normalizeOpenCodeTool } from './tool-normalizer.js';
4
+ import { RunnerUsageTracker, usageToken, usageTokenSum } from '../../service/runner-usage-tracker.js';
5
+ export class OpenCodeUsageNormalizer {
6
+ tracker;
7
+ messages = new Map();
8
+ constructor(identity, publish) {
9
+ this.tracker = new RunnerUsageTracker(identity, publish);
10
+ }
11
+ observe(value, startedAt) {
12
+ if (startedAt === undefined)
13
+ return;
14
+ const event = record(value), properties = record(event?.properties), info = record(properties?.info);
15
+ if (event?.type === 'message.updated' &&
16
+ info?.role === 'assistant' &&
17
+ typeof info.id === 'string' &&
18
+ typeof record(info.time)?.created === 'number' &&
19
+ Number(record(info.time)?.created) >= startedAt)
20
+ this.messages.set(info.id, {
21
+ ...(typeof info.modelID === 'string' ? { model: info.modelID } : {}),
22
+ ...(typeof info.providerID === 'string' ? { provider: info.providerID } : {})
23
+ });
24
+ const part = record(properties?.part);
25
+ if (event?.type !== 'message.part.updated' ||
26
+ part?.type !== 'step-finish' ||
27
+ typeof part.id !== 'string' ||
28
+ typeof part.messageID !== 'string')
29
+ return;
30
+ const model = this.messages.get(part.messageID);
31
+ if (!model)
32
+ return;
33
+ const tokens = record(part.tokens), cache = record(tokens?.cache);
34
+ const input = usageToken(tokens?.input), output = usageToken(tokens?.output), read = usageToken(cache?.read), write = usageToken(cache?.write), reasoning = usageToken(tokens?.reasoning);
35
+ const inputTokens = input === undefined ? undefined : usageTokenSum(input, read ?? '0', write ?? '0');
36
+ const outputTokens = output === undefined ? undefined : usageTokenSum(output, reasoning ?? '0');
37
+ this.tracker.snapshot(part.id, {
38
+ purpose: 'conversation',
39
+ agentRole: 'root',
40
+ ...(model.model ? { actualModel: model.model } : {}),
41
+ ...(model.provider ? { provider: model.provider } : {}),
42
+ ...(inputTokens === undefined ? {} : { inputTokens }),
43
+ ...(outputTokens === undefined ? {} : { outputTokens }),
44
+ ...(read === undefined ? {} : { cacheReadTokens: read }),
45
+ ...(write === undefined ? {} : { cacheWriteTokens: write }),
46
+ ...(reasoning === undefined ? {} : { reasoningTokens: reasoning })
47
+ }, true, 'partial');
48
+ }
49
+ finish(state) {
50
+ this.tracker.finish(state);
51
+ }
52
+ }
4
53
  export function openCodeUserMessagePreview(messages) {
5
54
  for (const message of messages) {
6
55
  const entry = record(message);
@@ -1,3 +1,4 @@
1
+ import { OpenCodeUsageNormalizer } from './conversation-parser.js';
1
2
  import { parseComposerTags } from '@myagentroam/protocol';
2
3
  import { nodeExecutionEnvironment } from '../../node-execution-environment.js';
3
4
  import { parseSecretEnvironment } from '../../util/secret-environment.js';
@@ -11,6 +12,7 @@ export class OpenCodeManagedRunController {
11
12
  runner;
12
13
  host;
13
14
  interactionTimeoutMs;
15
+ usage = new Map();
14
16
  permissions = new Map();
15
17
  questions = new Map();
16
18
  startingCwds = new Map();
@@ -316,6 +318,12 @@ export class OpenCodeManagedRunController {
316
318
  : undefined;
317
319
  if (sessionID !== nativeSessionId)
318
320
  return;
321
+ let usage = this.usage.get(runId);
322
+ if (!usage) {
323
+ usage = new OpenCodeUsageNormalizer(runId, (value) => this.host.emit(runId, 'run.usage', value));
324
+ this.usage.set(runId, usage);
325
+ }
326
+ usage.observe(value, execution?.responseStartedAt);
319
327
  if (event?.type === 'session.idle') {
320
328
  if (!this.promptAdmitted.has(runId)) {
321
329
  this.pendingIdle.add(runId);
@@ -630,6 +638,10 @@ export class OpenCodeManagedRunController {
630
638
  const client = execution?.client ?? this.startingClients.get(runId);
631
639
  if (execution === undefined && cwd === undefined && client === undefined)
632
640
  return;
641
+ this.usage
642
+ .get(runId)
643
+ ?.finish(status === 'SUCCEEDED' ? 'succeeded' : status === 'FAILED' ? 'failed' : 'cancelled');
644
+ this.usage.delete(runId);
633
645
  execution?.abort.abort();
634
646
  this.runner.execution.delete(runId);
635
647
  this.startingCwds.delete(runId);
@@ -328,6 +328,7 @@ export class ContinuousRunCoordinator {
328
328
  return;
329
329
  }
330
330
  this.options.control('run.start', {
331
+ ...(pending.usageGrant === undefined ? {} : { usageGrant: pending.usageGrant }),
331
332
  runId: executionId,
332
333
  sessionId: pending.sessionId,
333
334
  workspaceId: pending.workspaceId,
@@ -1,8 +1,9 @@
1
+ import { modelUsageGrantSchema } from '@myagentroam/protocol';
1
2
  import { createHash, randomUUID } from 'node:crypto';
2
3
  import { constants } from 'node:fs';
3
4
  import { lstat, mkdir, open, readFile, rename, rm, utimes } from 'node:fs/promises';
4
5
  import { isAbsolute, join, relative, resolve, sep } from 'node:path';
5
- import { conversationTurnSchema, conversationTemplateDraftSchema, conversationTemplateSummarySchema, hostedConversationConfigUpdateInputSchema, hostedConversationCreateInputSchema, hostedConversationDeleteResultSchema, hostedConversationFileDeleteInputSchema, hostedConversationFileDeleteResultSchema, hostedConversationFileReadInputSchema, hostedConversationFileReadResultSchema, hostedConversationFilesSchema, hostedConversationHistoryInputSchema, hostedConversationHistoryPageSchema, hostedConversationIdInputSchema, hostedConversationImageReadInputSchema, hostedConversationListInputSchema, hostedConversationListPageSchema, hostedConversationMessageInputSchema, hostedConversationMetadataUpdateInputSchema, hostedConversationRollbackInputSchema, hostedConversationSnapshotSchema, hostedConversationSummarySchema, hostedConversationUnwatchInputSchema, hostedConversationUploadChunkInputSchema, hostedConversationUploadControlInputSchema, hostedConversationUploadCreateInputSchema, hostedConversationUploadPreflightInputSchema, hostedConversationUploadPreflightResultSchema, hostedConversationUploadSchema, hostedConversationWatchInputSchema, marAgentRuntimeModelConfigurationSchema, mcpInstallationConfigurationSchema, mcpRuntimeDescriptorSchema, resolveMarAgentDefaultReasoningEffort } from '@myagentroam/protocol';
6
+ import { toolResultResourceInputSchema, toolResultResourcePageSchema, conversationTurnSchema, conversationTemplateDraftSchema, conversationTemplateSummarySchema, hostedConversationConfigUpdateInputSchema, hostedConversationCreateInputSchema, hostedConversationDeleteResultSchema, hostedConversationFileDeleteInputSchema, hostedConversationFileDeleteResultSchema, hostedConversationFileReadInputSchema, hostedConversationFileReadResultSchema, hostedConversationFilesSchema, hostedConversationHistoryInputSchema, hostedConversationHistoryPageSchema, hostedConversationIdInputSchema, hostedConversationImageReadInputSchema, hostedConversationListInputSchema, hostedConversationListPageSchema, hostedConversationMessageInputSchema, hostedConversationMetadataUpdateInputSchema, hostedConversationRollbackInputSchema, hostedConversationSnapshotSchema, hostedConversationSummarySchema, hostedConversationUnwatchInputSchema, hostedConversationUploadChunkInputSchema, hostedConversationUploadControlInputSchema, hostedConversationUploadCreateInputSchema, hostedConversationUploadPreflightInputSchema, hostedConversationUploadPreflightResultSchema, hostedConversationUploadSchema, hostedConversationWatchInputSchema, marAgentRuntimeModelConfigurationSchema, mcpInstallationConfigurationSchema, mcpRuntimeDescriptorSchema, resolveMarAgentDefaultReasoningEffort } from '@myagentroam/protocol';
6
7
  import { hostedConversationGeneratedImagesDisplayDirectory, hostedConversationImageRunId, marAgentGeneratedImagesDirectory } from '../runner/mar-agent/generated-images.js';
7
8
  import { sessionTitleFromFirstMessage } from '../util/node-operation-parsers.js';
8
9
  import { safeErrorCode } from '../util/safe-error.js';
@@ -70,6 +71,7 @@ export class HostedConversationNodeService {
70
71
  'conversation.hosted.config.update': (raw) => this.updateConfiguration(raw),
71
72
  'conversation.hosted.metadata.update': (raw) => this.updateMetadata(raw),
72
73
  'conversation.hosted.image.read': (raw) => this.readImage(raw),
74
+ 'conversation.hosted.tool-resource.read': (raw) => this.readToolResource(raw),
73
75
  'conversation.hosted.files.list': (raw) => this.listFiles(raw),
74
76
  'conversation.hosted.file.read': (raw) => this.readFile(raw),
75
77
  'conversation.hosted.file.delete': (raw) => this.deleteFile(raw),
@@ -503,6 +505,9 @@ export class HostedConversationNodeService {
503
505
  let runtimeEventsAccepted = false;
504
506
  const handle = await this.options.runtime
505
507
  .startRun({
508
+ ...(payload.usageGrant === undefined || this.options.modelUsage === undefined
509
+ ? {}
510
+ : { onModelUsage: (usage) => this.options.modelUsage(payload.usageGrant, usage) }),
506
511
  conversationId: conversation.id,
507
512
  sessionId: conversation.sessionId,
508
513
  clientMessageId: input.clientMessageId,
@@ -874,6 +879,25 @@ export class HostedConversationNodeService {
874
879
  files: this.options.store.listFiles(payload.userId, input.conversationId)
875
880
  });
876
881
  }
882
+ async readToolResource(raw) {
883
+ const payload = hostedRequest(raw);
884
+ const { conversationId } = hostedConversationIdInputSchema.parse(payload.data);
885
+ const conversation = this.options.store.requireConversation(payload.userId, conversationId);
886
+ const paths = this.options.store.pathsForConversation(payload.userId, conversation.id);
887
+ const data = payload.data;
888
+ const input = toolResultResourceInputSchema.parse({
889
+ resourceRef: data.resourceRef,
890
+ cursor: data.cursor,
891
+ maxBytes: data.maxBytes
892
+ });
893
+ if (!this.options.runtime.readToolResultResource)
894
+ throw new Error('TOOL_RESOURCE_UNSUPPORTED');
895
+ return toolResultResourcePageSchema.parse(await this.options.runtime.readToolResultResource({
896
+ homeDirectory: paths.homeDirectory,
897
+ sessionId: conversation.sessionId,
898
+ ...input
899
+ }));
900
+ }
877
901
  async readImage(raw) {
878
902
  const payload = hostedRequest(raw);
879
903
  const input = hostedConversationImageReadInputSchema.parse(payload.data);
@@ -1638,7 +1662,7 @@ export class HostedConversationNodeService {
1638
1662
  function hostedRequest(raw) {
1639
1663
  if (!record(raw))
1640
1664
  throw new Error('HOSTED_CONVERSATION_NOT_AUTHORIZED');
1641
- const { __requestUserId, __template, __marAgentModel, __marAgentModels, __marAgentCompact, __marAgentRuntimeScopeId, __workspaceWatchSessionHash, __templateDeleted, ...data } = raw;
1665
+ const { __usageGrant, __requestUserId, __template, __marAgentModel, __marAgentModels, __marAgentCompact, __marAgentRuntimeScopeId, __workspaceWatchSessionHash, __templateDeleted, ...data } = raw;
1642
1666
  if (typeof __requestUserId !== 'number' ||
1643
1667
  !Number.isSafeInteger(__requestUserId) ||
1644
1668
  __requestUserId <= 0)
@@ -1650,6 +1674,9 @@ function hostedRequest(raw) {
1650
1674
  throw new Error('HOSTED_CONVERSATION_NOT_AUTHORIZED');
1651
1675
  return {
1652
1676
  data,
1677
+ ...(__usageGrant === undefined
1678
+ ? {}
1679
+ : { usageGrant: modelUsageGrantSchema.parse(__usageGrant) }),
1653
1680
  userId: __requestUserId,
1654
1681
  ...(__template === undefined ? {} : { template: __template }),
1655
1682
  ...(__marAgentModel === undefined ? {} : { model: __marAgentModel }),
@@ -61,11 +61,26 @@ function modelTestErrorProjection(error) {
61
61
  if (typeof candidate.code !== 'string' || typeof candidate.message !== 'string')
62
62
  return runnerErrorProjection(error, 'MAR_AGENT_MODEL_CONNECTION_FAILED');
63
63
  const prefix = `${candidate.code}: `;
64
- return runnerErrorProjection({
64
+ const projection = runnerErrorProjection({
65
65
  code: candidate.code,
66
66
  message: candidate.message.startsWith(prefix)
67
67
  ? candidate.message.slice(prefix.length)
68
68
  : candidate.message,
69
69
  status: candidate.details?.['status']
70
70
  }, 'MAR_AGENT_MODEL_CONNECTION_FAILED');
71
+ const details = candidate.details;
72
+ return {
73
+ ...projection,
74
+ ...(typeof details?.['body'] === 'string' && typeof details['bodyTruncated'] === 'boolean'
75
+ ? {
76
+ response: {
77
+ body: details['body'],
78
+ bodyTruncated: details['bodyTruncated'],
79
+ contentType: details['contentType'] ?? null,
80
+ requestId: details['requestId'] ?? null,
81
+ retryAfter: details['retryAfter'] ?? null
82
+ }
83
+ }
84
+ : {})
85
+ };
71
86
  }