@renseiai/agentfactory 0.8.21 → 0.8.22

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.
@@ -25,12 +25,51 @@ import { spawn } from 'child_process';
25
25
  import { createInterface } from 'readline';
26
26
  import { classifyTool } from '../tools/tool-category.js';
27
27
  import { evaluateCommandApproval, evaluateFileChangeApproval, } from './codex-approval-bridge.js';
28
+ function isServerRequest(msg) {
29
+ return 'id' in msg && 'method' in msg;
30
+ }
28
31
  function isResponse(msg) {
29
- return 'id' in msg && typeof msg.id === 'number';
32
+ return 'id' in msg && !('method' in msg);
30
33
  }
31
34
  function isNotification(msg) {
32
35
  return 'method' in msg && !('id' in msg);
33
36
  }
37
+ // ---------------------------------------------------------------------------
38
+ // Codex model mapping (SUP-1749)
39
+ // ---------------------------------------------------------------------------
40
+ export const CODEX_MODEL_MAP = {
41
+ 'opus': 'gpt-5-codex',
42
+ 'sonnet': 'gpt-5.2-codex',
43
+ 'haiku': 'gpt-5.3-codex',
44
+ };
45
+ export const CODEX_DEFAULT_MODEL = 'gpt-5-codex';
46
+ export function resolveCodexModel(config) {
47
+ if (config.model)
48
+ return config.model;
49
+ const tier = config.env.CODEX_MODEL_TIER;
50
+ if (tier && CODEX_MODEL_MAP[tier])
51
+ return CODEX_MODEL_MAP[tier];
52
+ if (config.env.CODEX_MODEL)
53
+ return config.env.CODEX_MODEL;
54
+ return CODEX_DEFAULT_MODEL;
55
+ }
56
+ // ---------------------------------------------------------------------------
57
+ // Codex pricing and cost calculation (SUP-1750)
58
+ // ---------------------------------------------------------------------------
59
+ /** Codex pricing per 1M tokens (USD). Update when pricing changes. */
60
+ export const CODEX_PRICING = {
61
+ 'gpt-5-codex': { input: 2.00, cachedInput: 0.50, output: 8.00 },
62
+ 'gpt-5.2-codex': { input: 1.00, cachedInput: 0.25, output: 4.00 },
63
+ 'gpt-5.3-codex': { input: 0.50, cachedInput: 0.125, output: 2.00 },
64
+ };
65
+ export const CODEX_DEFAULT_PRICING = CODEX_PRICING['gpt-5-codex'];
66
+ export function calculateCostUsd(inputTokens, cachedInputTokens, outputTokens, model) {
67
+ const pricing = (model && CODEX_PRICING[model]) || CODEX_DEFAULT_PRICING;
68
+ const freshInputTokens = Math.max(0, inputTokens - cachedInputTokens);
69
+ return ((freshInputTokens / 1_000_000) * pricing.input +
70
+ (cachedInputTokens / 1_000_000) * pricing.cachedInput +
71
+ (outputTokens / 1_000_000) * pricing.output);
72
+ }
34
73
  /**
35
74
  * Manages a single long-lived `codex app-server` process.
36
75
  *
@@ -87,7 +126,12 @@ export class AppServerProcessManager {
87
126
  // Non-JSON output — ignore
88
127
  return;
89
128
  }
90
- if (isResponse(msg)) {
129
+ if (isServerRequest(msg)) {
130
+ // Server requests have both `id` and `method` — Codex expects a response.
131
+ // Approval requests (commandExecution/requestApproval, etc.) come as server requests.
132
+ this.handleServerRequest(msg);
133
+ }
134
+ else if (isResponse(msg)) {
91
135
  this.handleResponse(msg);
92
136
  }
93
137
  else if (isNotification(msg)) {
@@ -106,6 +150,16 @@ export class AppServerProcessManager {
106
150
  });
107
151
  // Perform initialization handshake
108
152
  await this.initialize();
153
+ // Discover available models (best effort — older servers may not support model/list)
154
+ try {
155
+ const models = await this.listModels();
156
+ if (models.length > 0) {
157
+ console.error(`[CodexAppServer] Available models: ${models.map(m => m.id).join(', ')}`);
158
+ }
159
+ }
160
+ catch {
161
+ console.error('[CodexAppServer] model/list not supported by this server version');
162
+ }
109
163
  }
110
164
  /**
111
165
  * JSON-RPC 2.0 initialization handshake:
@@ -174,6 +228,44 @@ export class AppServerProcessManager {
174
228
  pending.resolve(response.result);
175
229
  }
176
230
  }
231
+ /**
232
+ * Handle an incoming JSON-RPC server request (has both `id` and `method`).
233
+ * Codex sends approval requests as server requests that expect a response.
234
+ * Route to the thread listener (as a notification-like object) and store
235
+ * the request ID so the handle can respond.
236
+ */
237
+ handleServerRequest(request) {
238
+ const threadId = request.params?.threadId;
239
+ console.error(`[CodexAppServer] Server request: ${request.method} (id=${request.id}, thread=${threadId ?? 'none'})`);
240
+ // Wrap as a notification-compatible object for the thread listener,
241
+ // but include the id so the handle can respond.
242
+ const notificationLike = {
243
+ method: request.method,
244
+ params: { ...request.params, _serverRequestId: request.id },
245
+ };
246
+ if (threadId) {
247
+ const listener = this.threadListeners.get(threadId);
248
+ if (listener) {
249
+ listener(notificationLike);
250
+ return;
251
+ }
252
+ }
253
+ // No thread listener — auto-accept to avoid hanging
254
+ console.error(`[CodexAppServer] No thread listener for ${request.method} — auto-accepting`);
255
+ this.respondToServerRequest(request.id, { decision: 'acceptForSession' });
256
+ }
257
+ /**
258
+ * Send a JSON-RPC response to a server request.
259
+ */
260
+ respondToServerRequest(requestId, result) {
261
+ if (!this.process?.stdin?.writable) {
262
+ console.error('[CodexAppServer] Cannot respond to server request: stdin not writable');
263
+ return;
264
+ }
265
+ console.error(`[CodexAppServer] Responding to server request ${requestId}: ${JSON.stringify(result)}`);
266
+ const response = JSON.stringify({ jsonrpc: '2.0', id: requestId, result });
267
+ this.process.stdin.write(response + '\n');
268
+ }
177
269
  /**
178
270
  * Handle an incoming JSON-RPC notification.
179
271
  * Routes to the appropriate thread listener based on threadId in params.
@@ -236,8 +328,8 @@ export class AppServerProcessManager {
236
328
  }
237
329
  try {
238
330
  await this.request('config/batchWrite', {
239
- entries: [
240
- { key: 'mcpServers', value: mcpServers },
331
+ edits: [
332
+ { keyPath: 'mcpServers', mergeStrategy: 'replace', value: mcpServers },
241
333
  ],
242
334
  });
243
335
  this.mcpConfigured = true;
@@ -264,6 +356,13 @@ export class AppServerProcessManager {
264
356
  return [];
265
357
  }
266
358
  }
359
+ /**
360
+ * Discover available models from the app-server via model/list.
361
+ */
362
+ async listModels() {
363
+ const result = await this.request('model/list', {});
364
+ return result?.models ?? [];
365
+ }
267
366
  /**
268
367
  * Get the PID of the app-server process.
269
368
  */
@@ -359,6 +458,7 @@ export function mapAppServerNotification(notification, state) {
359
458
  if (turn?.usage) {
360
459
  state.totalInputTokens += turn.usage.input_tokens ?? 0;
361
460
  state.totalOutputTokens += turn.usage.output_tokens ?? 0;
461
+ state.totalCachedInputTokens += turn.usage.cached_input_tokens ?? 0;
362
462
  }
363
463
  if (turnStatus === 'completed') {
364
464
  return [{
@@ -367,6 +467,8 @@ export function mapAppServerNotification(notification, state) {
367
467
  cost: {
368
468
  inputTokens: state.totalInputTokens || undefined,
369
469
  outputTokens: state.totalOutputTokens || undefined,
470
+ cachedInputTokens: state.totalCachedInputTokens || undefined,
471
+ totalCostUsd: calculateCostUsd(state.totalInputTokens, state.totalCachedInputTokens, state.totalOutputTokens, state.model ?? undefined),
370
472
  numTurns: state.turnCount || undefined,
371
473
  },
372
474
  raw: notification,
@@ -381,6 +483,8 @@ export function mapAppServerNotification(notification, state) {
381
483
  cost: {
382
484
  inputTokens: state.totalInputTokens || undefined,
383
485
  outputTokens: state.totalOutputTokens || undefined,
486
+ cachedInputTokens: state.totalCachedInputTokens || undefined,
487
+ totalCostUsd: calculateCostUsd(state.totalInputTokens, state.totalCachedInputTokens, state.totalOutputTokens, state.model ?? undefined),
384
488
  numTurns: state.turnCount || undefined,
385
489
  },
386
490
  raw: notification,
@@ -408,7 +512,7 @@ export function mapAppServerNotification(notification, state) {
408
512
  return mapAppServerItemEvent(method, params);
409
513
  // --- Item deltas (streaming) ---
410
514
  case 'item/agentMessage/delta': {
411
- const text = params.text;
515
+ const text = (params.delta ?? params.text);
412
516
  if (text) {
413
517
  return [{
414
518
  type: 'assistant_text',
@@ -435,7 +539,7 @@ export function mapAppServerNotification(notification, state) {
435
539
  return [{
436
540
  type: 'system',
437
541
  subtype: 'command_progress',
438
- message: (params.delta ?? params.output) ?? '',
542
+ message: stripAnsi((params.delta ?? params.output) ?? ''),
439
543
  raw: notification,
440
544
  }];
441
545
  // --- Turn diff/plan ---
@@ -462,6 +566,16 @@ export function mapAppServerNotification(notification, state) {
462
566
  }];
463
567
  }
464
568
  }
569
+ /**
570
+ * Strip ANSI escape codes from text.
571
+ * Codex shell commands produce raw terminal output with color codes,
572
+ * cursor movement, etc. that pollute logs and activity tracking.
573
+ */
574
+ // eslint-disable-next-line no-control-regex
575
+ const ANSI_PATTERN = /\x1b\[[0-9;]*[a-zA-Z]|\x1b\].*?\x07|\x1b[()][AB012]|\x1b\[[\d;]*m/g;
576
+ function stripAnsi(text) {
577
+ return text.replace(ANSI_PATTERN, '');
578
+ }
465
579
  /**
466
580
  * Map item/started and item/completed notifications to AgentEvents.
467
581
  * Exported for unit testing.
@@ -507,7 +621,7 @@ export function mapAppServerItemEvent(method, params) {
507
621
  type: 'tool_result',
508
622
  toolName: 'shell',
509
623
  toolUseId: item.id,
510
- content: item.text ?? '',
624
+ content: stripAnsi(item.text ?? ''),
511
625
  isError: item.status === 'failed' || (item.exitCode !== undefined && item.exitCode !== 0),
512
626
  raw: { method, params },
513
627
  }];
@@ -607,20 +721,75 @@ export function normalizeMcpToolName(server, tool) {
607
721
  // Resolve approval policy from AgentSpawnConfig
608
722
  // ---------------------------------------------------------------------------
609
723
  function resolveApprovalPolicy(config) {
610
- // SUP-1747: Use 'onRequest' for autonomous agents so all tool executions
724
+ // SUP-1747: Use 'on-request' for autonomous agents so all tool executions
611
725
  // flow through the approval bridge for safety evaluation. The bridge
612
726
  // auto-approves safe commands and declines destructive patterns.
727
+ // Codex v0.117+ uses kebab-case: 'on-request' | 'untrusted' | 'on-failure' | 'never'
613
728
  if (config.autonomous)
614
- return 'onRequest';
615
- return 'unlessTrusted';
729
+ return 'on-request';
730
+ return 'untrusted';
616
731
  }
617
- function resolveSandboxPolicy(config) {
732
+ /**
733
+ * Map AgentSpawnConfig sandbox settings to Codex App Server sandbox policy.
734
+ *
735
+ * Codex sandbox levels vs Claude sandbox:
736
+ * | Feature | Claude | Codex |
737
+ * |-----------------------|-------------------------|--------------------------------|
738
+ * | File write control | Per-file glob patterns | Workspace root only |
739
+ * | Network access | Per-domain allow-lists | All-or-nothing per level |
740
+ * | Tool-level permissions| Per-tool allow/deny | Not supported (approval policy)|
741
+ * | Custom writable paths | Multiple glob patterns | Single writableRoots array |
742
+ * | Process isolation | macOS sandbox-exec | Docker/firewall container |
743
+ *
744
+ * Key limitation: Codex cannot restrict writes to specific subdirectories within
745
+ * the workspace or allow network access to specific domains. The mapping is intent-based:
746
+ * "safe browsing/analysis" → readOnly
747
+ * "normal development" → workspaceWrite
748
+ * "install/deploy/admin" → dangerFullAccess
749
+ */
750
+ /**
751
+ * Resolve sandbox policy as an object for turn/start (supports writableRoots).
752
+ * Codex v0.117+ turn/start accepts: { type: 'workspaceWrite', writableRoots: [...] }
753
+ *
754
+ * Network access is enabled by default for agents because they need to run
755
+ * commands like `gh`, `curl`, `pnpm install`, etc. The sandbox still restricts
756
+ * file writes to the workspace root.
757
+ */
758
+ export function resolveSandboxPolicy(config) {
759
+ if (config.sandboxLevel) {
760
+ switch (config.sandboxLevel) {
761
+ case 'read-only':
762
+ return { type: 'readOnly', networkAccess: true };
763
+ case 'workspace-write':
764
+ return { type: 'workspaceWrite', writableRoots: [config.cwd], networkAccess: true };
765
+ case 'full-access':
766
+ return { type: 'dangerFullAccess' };
767
+ }
768
+ }
769
+ // Fallback: boolean sandboxEnabled → workspaceWrite with network
618
770
  if (!config.sandboxEnabled)
619
771
  return undefined;
620
- return {
621
- type: 'workspaceWrite',
622
- writableRoots: [config.cwd],
623
- };
772
+ return { type: 'workspaceWrite', writableRoots: [config.cwd], networkAccess: true };
773
+ }
774
+ /**
775
+ * Resolve sandbox mode as a simple string for thread/start.
776
+ * Codex v0.117+ thread/start accepts: 'read-only' | 'workspace-write' | 'danger-full-access'
777
+ */
778
+ export function resolveSandboxMode(config) {
779
+ if (config.sandboxLevel) {
780
+ switch (config.sandboxLevel) {
781
+ case 'read-only':
782
+ return 'read-only';
783
+ case 'workspace-write':
784
+ return 'workspace-write';
785
+ case 'full-access':
786
+ return 'danger-full-access';
787
+ }
788
+ }
789
+ // Fallback: boolean sandboxEnabled → workspace-write
790
+ if (!config.sandboxEnabled)
791
+ return undefined;
792
+ return 'workspace-write';
624
793
  }
625
794
  // ---------------------------------------------------------------------------
626
795
  // Base Instructions Builder (SUP-1746)
@@ -663,8 +832,10 @@ class AppServerAgentHandle {
663
832
  resumeThreadId;
664
833
  mapperState = {
665
834
  sessionId: null,
835
+ model: null,
666
836
  totalInputTokens: 0,
667
837
  totalOutputTokens: 0,
838
+ totalCachedInputTokens: 0,
668
839
  turnCount: 0,
669
840
  };
670
841
  activeTurnId = null;
@@ -673,6 +844,8 @@ class AppServerAgentHandle {
673
844
  streamEnded = false;
674
845
  /** True while we're waiting for a possible injected turn between turns */
675
846
  awaitingInjection = false;
847
+ /** Accumulated assistant text for the result message (completion comment) */
848
+ accumulatedText = '';
676
849
  constructor(processManager, config, resumeThreadId) {
677
850
  this.processManager = processManager;
678
851
  this.config = config;
@@ -725,7 +898,7 @@ class AppServerAgentHandle {
725
898
  }
726
899
  await this.processManager.request('turn/steer', {
727
900
  threadId: this.sessionId,
728
- turnId: this.activeTurnId,
901
+ expectedTurnId: this.activeTurnId,
729
902
  input: [{ type: 'text', text }],
730
903
  });
731
904
  }
@@ -738,9 +911,10 @@ class AppServerAgentHandle {
738
911
  *
739
912
  * Returns a system event if the request was declined, for observability.
740
913
  */
741
- async handleApprovalRequest(notification) {
914
+ handleApprovalRequest(notification) {
742
915
  const params = notification.params ?? {};
743
- const requestId = params.requestId;
916
+ // Server requests pass _serverRequestId; fall back to requestId for backwards compat
917
+ const serverRequestId = params._serverRequestId;
744
918
  const command = params.command;
745
919
  const filePath = params.filePath;
746
920
  let decision;
@@ -756,13 +930,14 @@ class AppServerAgentHandle {
756
930
  // Unknown approval request — accept by default
757
931
  decision = { action: 'acceptForSession' };
758
932
  }
759
- // Respond to the App Server with the approval decision
760
- await this.processManager.request('approval/respond', {
761
- threadId: this.sessionId,
762
- requestId,
763
- decision: decision.action,
764
- reason: decision.reason,
765
- });
933
+ // Respond to the server request with the approval decision.
934
+ // Codex sends approval requests as JSON-RPC server requests (with `id`),
935
+ // expecting a JSON-RPC response matching that id.
936
+ if (serverRequestId != null) {
937
+ this.processManager.respondToServerRequest(serverRequestId, {
938
+ decision: decision.action,
939
+ });
940
+ }
766
941
  // Emit system event for declined approvals (observability)
767
942
  if (decision.action === 'decline') {
768
943
  const target = command ?? filePath ?? 'unknown';
@@ -789,9 +964,7 @@ class AppServerAgentHandle {
789
964
  cwd: this.config.cwd,
790
965
  approvalPolicy: resolveApprovalPolicy(this.config),
791
966
  };
792
- if (this.config.maxTurns) {
793
- turnParams.maxTurns = this.config.maxTurns;
794
- }
967
+ turnParams.model = resolveCodexModel(this.config);
795
968
  const sandboxPolicy = resolveSandboxPolicy(this.config);
796
969
  if (sandboxPolicy) {
797
970
  turnParams.sandboxPolicy = sandboxPolicy;
@@ -818,7 +991,7 @@ class AppServerAgentHandle {
818
991
  // Resume existing thread
819
992
  const result = await this.processManager.request('thread/resume', {
820
993
  threadId: this.resumeThreadId,
821
- personality: 'concise',
994
+ personality: 'pragmatic',
822
995
  });
823
996
  threadId = result?.thread?.id ?? this.resumeThreadId;
824
997
  }
@@ -829,15 +1002,17 @@ class AppServerAgentHandle {
829
1002
  approvalPolicy: resolveApprovalPolicy(this.config),
830
1003
  serviceName: 'agentfactory',
831
1004
  };
832
- // SUP-1746: Pass persistent system instructions via `instructions` on thread/start.
1005
+ // SUP-1746: Pass persistent system instructions via `baseInstructions` on thread/start.
833
1006
  // Separates safety rules and project context from per-turn task input.
834
1007
  const instructions = buildBaseInstructions(this.config);
835
1008
  if (instructions) {
836
- threadParams.instructions = instructions;
1009
+ threadParams.baseInstructions = instructions;
837
1010
  }
838
- const sandboxPolicy = resolveSandboxPolicy(this.config);
839
- if (sandboxPolicy) {
840
- threadParams.sandboxPolicy = sandboxPolicy;
1011
+ threadParams.model = resolveCodexModel(this.config);
1012
+ // thread/start uses simple string sandbox mode (not object like turn/start)
1013
+ const sandboxMode = resolveSandboxMode(this.config);
1014
+ if (sandboxMode) {
1015
+ threadParams.sandbox = sandboxMode;
841
1016
  }
842
1017
  const result = await this.processManager.request('thread/start', threadParams);
843
1018
  threadId = result?.thread?.id ?? '';
@@ -852,6 +1027,7 @@ class AppServerAgentHandle {
852
1027
  }
853
1028
  this.sessionId = threadId;
854
1029
  this.mapperState.sessionId = threadId;
1030
+ this.mapperState.model = resolveCodexModel(this.config);
855
1031
  // Subscribe to thread notifications
856
1032
  this.processManager.subscribeThread(threadId, (notification) => {
857
1033
  this.notificationQueue.push(notification);
@@ -873,9 +1049,7 @@ class AppServerAgentHandle {
873
1049
  cwd: this.config.cwd,
874
1050
  approvalPolicy: resolveApprovalPolicy(this.config),
875
1051
  };
876
- if (this.config.maxTurns) {
877
- turnParams.maxTurns = this.config.maxTurns;
878
- }
1052
+ turnParams.model = resolveCodexModel(this.config);
879
1053
  const sandboxPolicy = resolveSandboxPolicy(this.config);
880
1054
  if (sandboxPolicy) {
881
1055
  turnParams.sandboxPolicy = sandboxPolicy;
@@ -905,9 +1079,11 @@ class AppServerAgentHandle {
905
1079
  while (this.notificationQueue.length > 0) {
906
1080
  const notification = this.notificationQueue.shift();
907
1081
  // SUP-1747: Intercept approval requests before other processing.
908
- // The App Server emits these when approvalPolicy is 'onRequest'.
909
- if (notification.method.endsWith('/requestApproval')) {
910
- const deniedEvent = await this.handleApprovalRequest(notification);
1082
+ // Codex sends approvals as server requests with methods like:
1083
+ // item/commandExecution/requestApproval, item/fileChange/requestApproval,
1084
+ // item/permissions/requestApproval, applyPatchApproval, execCommandApproval
1085
+ if (notification.method.includes('pproval') || notification.method.includes('requestApproval')) {
1086
+ const deniedEvent = this.handleApprovalRequest(notification);
911
1087
  if (deniedEvent) {
912
1088
  yield deniedEvent;
913
1089
  }
@@ -928,18 +1104,31 @@ class AppServerAgentHandle {
928
1104
  }
929
1105
  const events = mapAppServerNotification(notification, this.mapperState);
930
1106
  for (const event of events) {
931
- // Intercept turn/completed result events — convert to system events
932
- // so the orchestrator doesn't think the agent is done. Track the last
933
- // turn's outcome so we can emit a proper result when the stream ends.
1107
+ // Intercept turn/completed result events.
1108
+ // In autonomous mode (fleet), emit the result directly to end the session.
1109
+ // In interactive mode, convert to system event to keep the stream alive
1110
+ // for potential message injection.
1111
+ // Accumulate assistant text for the result message / completion comment
1112
+ if (event.type === 'assistant_text' && event.text) {
1113
+ this.accumulatedText += event.text;
1114
+ }
934
1115
  if (event.type === 'result') {
935
- lastTurnSuccess = event.success;
936
- lastTurnErrors = event.errors;
937
- yield {
938
- type: 'system',
939
- subtype: 'turn_result',
940
- message: `Turn ${event.success ? 'succeeded' : 'failed'}${event.errors?.length ? ': ' + event.errors[0] : ''}`,
941
- raw: event.raw,
942
- };
1116
+ if (this.config.autonomous) {
1117
+ // Autonomous: emit result with accumulated text and end stream
1118
+ yield { ...event, message: this.accumulatedText.trim() || undefined };
1119
+ this.streamEnded = true;
1120
+ }
1121
+ else {
1122
+ // Interactive: keep stream alive for injection
1123
+ lastTurnSuccess = event.success;
1124
+ lastTurnErrors = event.errors;
1125
+ yield {
1126
+ type: 'system',
1127
+ subtype: 'turn_result',
1128
+ message: `Turn ${event.success ? 'succeeded' : 'failed'}${event.errors?.length ? ': ' + event.errors[0] : ''}`,
1129
+ raw: event.raw,
1130
+ };
1131
+ }
943
1132
  }
944
1133
  else {
945
1134
  yield event;
@@ -955,14 +1144,17 @@ class AppServerAgentHandle {
955
1144
  catch {
956
1145
  // Best effort
957
1146
  }
958
- // Emit the final result event when the stream ends
1147
+ // Emit the final result event when the stream ends (interactive mode / stop)
959
1148
  yield {
960
1149
  type: 'result',
961
1150
  success: lastTurnSuccess,
1151
+ message: this.accumulatedText.trim() || undefined,
962
1152
  errors: lastTurnErrors,
963
1153
  cost: {
964
1154
  inputTokens: this.mapperState.totalInputTokens || undefined,
965
1155
  outputTokens: this.mapperState.totalOutputTokens || undefined,
1156
+ cachedInputTokens: this.mapperState.totalCachedInputTokens || undefined,
1157
+ totalCostUsd: calculateCostUsd(this.mapperState.totalInputTokens, this.mapperState.totalCachedInputTokens, this.mapperState.totalOutputTokens, this.mapperState.model ?? undefined),
966
1158
  numTurns: this.mapperState.turnCount || undefined,
967
1159
  },
968
1160
  raw: null,