praxis-agent 0.16.0 → 0.17.0

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 (43) hide show
  1. package/README.md +11 -5
  2. package/dist/application/background-agent-manager.d.ts +7 -1
  3. package/dist/application/background-agent-manager.js +148 -5
  4. package/dist/application/metered-model-completion.d.ts +15 -0
  5. package/dist/application/metered-model-completion.js +76 -0
  6. package/dist/application/model-compactor.js +25 -1
  7. package/dist/application/session-cost-tracker.d.ts +50 -0
  8. package/dist/application/session-cost-tracker.js +254 -0
  9. package/dist/application/session-service.d.ts +13 -0
  10. package/dist/application/session-service.js +594 -69
  11. package/dist/application/subagent-service.d.ts +11 -1
  12. package/dist/application/subagent-service.js +41 -1
  13. package/dist/application/tool-use-summary.d.ts +7 -2
  14. package/dist/application/tool-use-summary.js +42 -10
  15. package/dist/application/workflow-manager.d.ts +9 -4
  16. package/dist/application/workflow-manager.js +151 -7
  17. package/dist/cli/interactive.d.ts +5 -1
  18. package/dist/cli/interactive.js +227 -15
  19. package/dist/cli/protocol.d.ts +11 -0
  20. package/dist/cli/protocol.js +16 -1
  21. package/dist/cli/tui/claude-command-inventory.d.ts +27 -27
  22. package/dist/cli/tui/claude-command-inventory.js +26 -26
  23. package/dist/cli/tui/config-dashboard.d.ts +2 -9
  24. package/dist/cli/tui/config-dashboard.js +6 -6
  25. package/dist/cli/tui/cost-summary.d.ts +34 -0
  26. package/dist/cli/tui/cost-summary.js +192 -0
  27. package/dist/cli/tui/doctor-dashboard.d.ts +15 -0
  28. package/dist/cli/tui/doctor-dashboard.js +81 -0
  29. package/dist/cli/tui/slash-commands.js +10 -0
  30. package/dist/cli.d.ts +2 -0
  31. package/dist/cli.js +192 -15
  32. package/dist/core/compaction.d.ts +2 -0
  33. package/dist/core/runtime.d.ts +38 -1
  34. package/dist/core/runtime.js +265 -11
  35. package/dist/persistence/claude-cost-state-store.d.ts +48 -0
  36. package/dist/persistence/claude-cost-state-store.js +335 -0
  37. package/dist/providers/anthropic-compatible.js +34 -9
  38. package/dist/providers/fallback-provider.d.ts +2 -1
  39. package/dist/providers/fallback-provider.js +28 -3
  40. package/dist/tools/line-changes.d.ts +8 -0
  41. package/dist/tools/line-changes.js +35 -0
  42. package/dist/tools/local-tools.js +30 -1
  43. package/package.json +2 -1
package/dist/cli.js CHANGED
@@ -7,15 +7,17 @@ import { dirname, join, resolve } from 'node:path';
7
7
  import { fileURLToPath, pathToFileURL } from 'node:url';
8
8
  import { randomUUID } from 'node:crypto';
9
9
  import { ClaudeSessionService, } from './application/session-service.js';
10
+ import { ClaudeCostStateStore } from './persistence/claude-cost-state-store.js';
10
11
  import { agentColorMessage, parseAgentColorInput, } from './compatibility/claude/agent-color.js';
11
12
  import { ClaudeConditionalRuleResolver, ClaudeContextAssembler, } from './compatibility/claude/context.js';
12
13
  import { loadClaudeDynamicContext } from './compatibility/claude/dynamic-context.js';
13
14
  import { createClaudePrSessionFilter, filterClaudePrLinkedSessions, } from './compatibility/claude/pr-links.js';
14
15
  import { isClaudeSessionId, resolveClaudePaths, } from './compatibility/claude/paths.js';
15
- import { loadClaudeContextResources, loadClaudeSettings, loadClaudeSharedResources, resolveClaudeProjectMemoryDirectory, } from './compatibility/claude/shared-resources.js';
16
+ import { loadClaudeContextResources, loadClaudeSettings, loadClaudeSharedResources, resolveClaudeProjectIdentity, resolveClaudeProjectMemoryDirectory, } from './compatibility/claude/shared-resources.js';
16
17
  import { AgentRunCancelledError, } from './core/runtime.js';
17
18
  import { persistTuiPermissionUpdates } from './cli/tui/permission-settings.js';
18
19
  import { loadClaudeReleaseNotes } from './cli/tui/release-notes.js';
20
+ import { canonicalClaudeCostModelName, formatCostSummary, } from './cli/tui/cost-summary.js';
19
21
  import { projectTuiHooks, } from './cli/tui/hook-settings.js';
20
22
  import { DEFAULT_CLI_CONTROLS, resolveCliControls } from './cli/controls.js';
21
23
  import { applyRuntimeSettingDefaults, loadRuntimeSettings, runtimeSettingsSystemPrompt, } from './cli/tui/runtime-settings.js';
@@ -47,7 +49,7 @@ import { WorkspaceContext } from './application/session-worktree.js';
47
49
  import { launchTmuxWorktree } from './platform/tmux-worktree.js';
48
50
  import { claudeSandboxRuntime } from './sandbox/claude-sandbox-runtime.js';
49
51
  import { claudeSandboxTempDirectory, loadClaudeSandboxSettings, } from './sandbox/claude-sandbox-settings.js';
50
- import { createErrorResult, createSuccessResult, matchHeadlessColorCommand, parseCliInvocation, readStreamJsonMessages, StreamJsonOutput, } from './cli/protocol.js';
52
+ import { createErrorResult, createSuccessResult, isHeadlessCostCommand, matchHeadlessColorCommand, parseCliInvocation, readStreamJsonMessages, StreamJsonOutput, } from './cli/protocol.js';
51
53
  import { describeClaudePlugin, initClaudePlugin, installClaudePlugin, loadClaudePlugins, readPluginRegistry, setClaudePluginEnabled, uninstallClaudePlugin, updateClaudePlugin, validateClaudePlugin, } from './plugins/claude-plugin-runtime.js';
52
54
  import { addClaudeMarketplace, disableAllNativePlugins, installClaudeMarketplacePlugin, listClaudeMarketplaceAvailablePlugins, listNativePluginRecords, readClaudeKnownMarketplaces, removeClaudeMarketplace, setNativePluginEnabled, saveClaudePluginConfig, uninstallNativePlugin, updateClaudeMarketplace, updateNativePlugin, validateClaudeMarketplace, } from './plugins/claude-plugin-marketplace.js';
53
55
  import { executeClaudePluginEvalCommand, PLUGIN_EVAL_HELP, } from './plugins/claude-plugin-eval.js';
@@ -770,12 +772,17 @@ const createDefaultService = async ({ eventSink, requireProvider, hooksOnly = fa
770
772
  };
771
773
  provider = providerForMainModel(model);
772
774
  }
775
+ const costStateStore = new ClaudeCostStateStore({
776
+ statePath: claudeStatePath,
777
+ projectIdentity: await resolveClaudeProjectIdentity({ cwd }),
778
+ });
773
779
  const options = {
774
780
  configRoot,
775
781
  cwd,
776
782
  claudeVersion,
777
783
  eventSink: runtimeEventSink,
778
784
  sessionPersistence: cli.sessionPersistence,
785
+ costStateStore,
779
786
  explicitModel: interactiveModel !== undefined || controls.model !== undefined,
780
787
  explicitSystemPrompt: cli.systemPrompt !== undefined,
781
788
  agentInitialPromptHandledExternally: interactive,
@@ -1293,6 +1300,9 @@ const createDefaultService = async ({ eventSink, requireProvider, hooksOnly = fa
1293
1300
  ...(provider?.capabilities.contextWindowTokens === undefined
1294
1301
  ? {}
1295
1302
  : { contextWindowTokens: provider.capabilities.contextWindowTokens }),
1303
+ ...(provider?.capabilities.maxOutputTokens === undefined
1304
+ ? {}
1305
+ : { maxOutputTokens: provider.capabilities.maxOutputTokens }),
1296
1306
  tools: toolNames,
1297
1307
  mcpServers: mcpTools.serverStatuses(),
1298
1308
  permissionMode: cli.dangerouslySkipPermissions
@@ -1382,6 +1392,7 @@ const createDefaultService = async ({ eventSink, requireProvider, hooksOnly = fa
1382
1392
  }
1383
1393
  },
1384
1394
  sessions: () => service.sessions(),
1395
+ costSnapshot: (sessionId) => service.costSnapshot(sessionId),
1385
1396
  slashCommands: () => extensions.slashCommandDefinitions().map((definition) => ({
1386
1397
  name: definition.name,
1387
1398
  description: definition.description,
@@ -1433,14 +1444,32 @@ const createDefaultService = async ({ eventSink, requireProvider, hooksOnly = fa
1433
1444
  if (failure !== undefined)
1434
1445
  throw failure;
1435
1446
  },
1436
- runtimeInfo: () => ({
1437
- ...runtimeInfo,
1438
- cwd: workspace.cwd(),
1439
- model: service.model() ??
1440
- provider?.model ??
1441
- runtimeEnvironment.PRAXIS_MODEL ??
1442
- 'unknown',
1443
- }),
1447
+ runtimeInfo: () => {
1448
+ // Drop the static capability snapshot so a fallback-routed provider
1449
+ // never reports stale context/output limits, then re-derive them from
1450
+ // the currently active provider.
1451
+ const staticInfo = {
1452
+ ...runtimeInfo,
1453
+ cwd: workspace.cwd(),
1454
+ model: service.model() ??
1455
+ provider?.model ??
1456
+ runtimeEnvironment.PRAXIS_MODEL ??
1457
+ 'unknown',
1458
+ };
1459
+ delete staticInfo.contextWindowTokens;
1460
+ delete staticInfo.maxOutputTokens;
1461
+ return {
1462
+ ...staticInfo,
1463
+ ...(provider?.capabilities.contextWindowTokens === undefined
1464
+ ? {}
1465
+ : {
1466
+ contextWindowTokens: provider.capabilities.contextWindowTokens,
1467
+ }),
1468
+ ...(provider?.capabilities.maxOutputTokens === undefined
1469
+ ? {}
1470
+ : { maxOutputTokens: provider.capabilities.maxOutputTokens }),
1471
+ };
1472
+ },
1444
1473
  promptSuggestion: (sessionId, suggestionSignal) => service.promptSuggestion(sessionId, suggestionSignal),
1445
1474
  };
1446
1475
  }
@@ -3266,6 +3295,104 @@ function isCancellation(error, signal) {
3266
3295
  signal?.aborted === true ||
3267
3296
  (error instanceof DOMException && error.name === 'AbortError'));
3268
3297
  }
3298
+ function emptyCostSnapshot(sessionId) {
3299
+ return {
3300
+ sessionId,
3301
+ totalCostUsd: 0,
3302
+ apiDurationMs: 0,
3303
+ apiDurationWithoutRetriesMs: 0,
3304
+ toolDurationMs: 0,
3305
+ wallDurationMs: 0,
3306
+ linesAdded: 0,
3307
+ linesRemoved: 0,
3308
+ modelUsage: {},
3309
+ hasUnknownModelCost: false,
3310
+ };
3311
+ }
3312
+ /**
3313
+ * Computes the validated nonnegative delta between a current cost snapshot and
3314
+ * a process-local baseline. The baseline represents restored historic state or
3315
+ * the empty state before this process touched the session, so the delta shows
3316
+ * only same-process accumulation. Counter regression and incompatible model
3317
+ * state are rejected explicitly rather than clamped.
3318
+ */
3319
+ function costSummaryDelta(current, baseline) {
3320
+ const base = baseline ?? emptyCostSnapshot(current.sessionId);
3321
+ const assertAtLeast = (label, value, floor) => {
3322
+ if (value < floor) {
3323
+ throw new Error(`Cost state regression: ${label} fell below the process baseline`);
3324
+ }
3325
+ return value - floor;
3326
+ };
3327
+ const totalCostUsd = assertAtLeast('total cost', current.totalCostUsd, base.totalCostUsd);
3328
+ const apiDurationMs = assertAtLeast('API duration', current.apiDurationMs, base.apiDurationMs);
3329
+ const wallDurationMs = assertAtLeast('wall duration', current.wallDurationMs, base.wallDurationMs);
3330
+ const linesAdded = assertAtLeast('lines added', current.linesAdded, base.linesAdded);
3331
+ const linesRemoved = assertAtLeast('lines removed', current.linesRemoved, base.linesRemoved);
3332
+ const modelUsage = [];
3333
+ for (const [model, usage] of Object.entries(current.modelUsage)) {
3334
+ const baseUsage = base.modelUsage[model];
3335
+ if (baseUsage === undefined) {
3336
+ modelUsage.push({
3337
+ model,
3338
+ canonicalName: canonicalClaudeCostModelName(model),
3339
+ inputTokens: usage.inputTokens,
3340
+ outputTokens: usage.outputTokens,
3341
+ cacheReadInputTokens: usage.cacheReadInputTokens,
3342
+ cacheCreationInputTokens: usage.cacheCreationInputTokens,
3343
+ webSearchRequests: usage.webSearchRequests,
3344
+ costUsd: usage.costUsd,
3345
+ });
3346
+ continue;
3347
+ }
3348
+ const inputTokens = usage.inputTokens - baseUsage.inputTokens;
3349
+ const outputTokens = usage.outputTokens - baseUsage.outputTokens;
3350
+ const cacheReadInputTokens = usage.cacheReadInputTokens - baseUsage.cacheReadInputTokens;
3351
+ const cacheCreationInputTokens = usage.cacheCreationInputTokens - baseUsage.cacheCreationInputTokens;
3352
+ const webSearchRequests = usage.webSearchRequests - baseUsage.webSearchRequests;
3353
+ const costUsd = usage.costUsd - baseUsage.costUsd;
3354
+ if (inputTokens < 0 ||
3355
+ outputTokens < 0 ||
3356
+ cacheReadInputTokens < 0 ||
3357
+ cacheCreationInputTokens < 0 ||
3358
+ webSearchRequests < 0 ||
3359
+ costUsd < 0) {
3360
+ throw new Error(`Cost state regression for model ${model}: counters fell below the process baseline`);
3361
+ }
3362
+ if (inputTokens === 0 &&
3363
+ outputTokens === 0 &&
3364
+ cacheReadInputTokens === 0 &&
3365
+ cacheCreationInputTokens === 0 &&
3366
+ webSearchRequests === 0 &&
3367
+ costUsd === 0) {
3368
+ continue;
3369
+ }
3370
+ modelUsage.push({
3371
+ model,
3372
+ canonicalName: canonicalClaudeCostModelName(model),
3373
+ inputTokens,
3374
+ outputTokens,
3375
+ cacheReadInputTokens,
3376
+ cacheCreationInputTokens,
3377
+ webSearchRequests,
3378
+ costUsd,
3379
+ });
3380
+ }
3381
+ for (const model of Object.keys(base.modelUsage)) {
3382
+ if (current.modelUsage[model] === undefined) {
3383
+ throw new Error(`Cost state regression for model ${model}: model disappeared from the session tracker`);
3384
+ }
3385
+ }
3386
+ return {
3387
+ totalCostUsd,
3388
+ apiDurationMs,
3389
+ wallDurationMs,
3390
+ linesAdded,
3391
+ linesRemoved,
3392
+ hasUnknownModelCost: current.hasUnknownModelCost && !base.hasUnknownModelCost,
3393
+ modelUsage,
3394
+ };
3395
+ }
3269
3396
  function helpActionAt(argv, commandIndex, actions) {
3270
3397
  for (let index = commandIndex + 1; index < argv.length; index += 1) {
3271
3398
  const value = argv[index];
@@ -3951,6 +4078,9 @@ async function execute(argv, io, dependencies, signal) {
3951
4078
  const firstTurnIsLocalColor = !invocation.disableSlashCommands &&
3952
4079
  firstHeadlessPrompt !== undefined &&
3953
4080
  matchHeadlessColorCommand(firstHeadlessPrompt) !== undefined;
4081
+ const firstTurnIsLocalCost = !invocation.disableSlashCommands &&
4082
+ firstHeadlessPrompt !== undefined &&
4083
+ isHeadlessCostCommand(firstHeadlessPrompt);
3954
4084
  const service = await dependencies.createService({
3955
4085
  eventSink: outputFormat === 'stream-json' && !invocation.legacyJson
3956
4086
  ? (event) => {
@@ -3975,7 +4105,10 @@ async function execute(argv, io, dependencies, signal) {
3975
4105
  }
3976
4106
  }
3977
4107
  : eventSink(io, outputFormat, invocation.legacyJson),
3978
- requireProvider: !streamInputExhausted && !firstTurnIsLocalColor && headlessTurnReached,
4108
+ requireProvider: !streamInputExhausted &&
4109
+ !firstTurnIsLocalColor &&
4110
+ !firstTurnIsLocalCost &&
4111
+ headlessTurnReached,
3979
4112
  ...(retryInterruptedTools ? { approveRecovery: () => true } : {}),
3980
4113
  ...(streamIterator ? { approveTool: approveStreamTool } : {}),
3981
4114
  ...(streamIterator ? { onElicitation: respondStreamElicitation } : {}),
@@ -3984,6 +4117,12 @@ async function execute(argv, io, dependencies, signal) {
3984
4117
  ...(agent ? { agent } : {}),
3985
4118
  controls: invocation,
3986
4119
  });
4120
+ const costBaselines = new Map();
4121
+ const ensureCostBaseline = async (sessionId) => {
4122
+ if (!service.costSnapshot || costBaselines.has(sessionId))
4123
+ return;
4124
+ costBaselines.set(sessionId, await service.costSnapshot(sessionId));
4125
+ };
3987
4126
  try {
3988
4127
  const lifecycleTrigger = invocation.init
3989
4128
  ? 'init'
@@ -4101,6 +4240,7 @@ async function execute(argv, io, dependencies, signal) {
4101
4240
  signal?.addEventListener('abort', forwardAbort, { once: true });
4102
4241
  currentTurnAbort = streamIterator ? turnAbort : undefined;
4103
4242
  const runSignal = streamIterator ? turnAbort.signal : signal;
4243
+ await ensureCostBaseline(activeSessionId);
4104
4244
  if (streamOutput) {
4105
4245
  streamOutput.init();
4106
4246
  if (isFirstTurn) {
@@ -4112,11 +4252,46 @@ async function execute(argv, io, dependencies, signal) {
4112
4252
  }
4113
4253
  let result;
4114
4254
  let colorArgs;
4255
+ let costTurn = false;
4115
4256
  try {
4257
+ costTurn =
4258
+ !invocation.disableSlashCommands && isHeadlessCostCommand(prompt);
4116
4259
  colorArgs = invocation.disableSlashCommands
4117
4260
  ? undefined
4118
4261
  : matchHeadlessColorCommand(prompt);
4119
- if (colorArgs !== undefined) {
4262
+ if (costTurn) {
4263
+ if (!service.costSnapshot) {
4264
+ throw new Error('Session cost is unavailable.');
4265
+ }
4266
+ const current = await service.costSnapshot(activeSessionId);
4267
+ const delta = costSummaryDelta(current, costBaselines.get(activeSessionId));
4268
+ const modelUsageEntries = delta.modelUsage.map((entry) => [
4269
+ entry.model,
4270
+ {
4271
+ inputTokens: entry.inputTokens,
4272
+ outputTokens: entry.outputTokens,
4273
+ ...(entry.cacheReadInputTokens === undefined
4274
+ ? {}
4275
+ : { cacheReadInputTokens: entry.cacheReadInputTokens }),
4276
+ ...(entry.cacheCreationInputTokens === undefined
4277
+ ? {}
4278
+ : { cacheCreationInputTokens: entry.cacheCreationInputTokens }),
4279
+ ...(entry.webSearchRequests === undefined
4280
+ ? {}
4281
+ : { webSearchRequests: entry.webSearchRequests }),
4282
+ },
4283
+ ]);
4284
+ result = {
4285
+ sessionId: activeSessionId,
4286
+ text: formatCostSummary(delta),
4287
+ usage: { inputTokens: 0, outputTokens: 0 },
4288
+ durationApiMs: 0,
4289
+ costUsd: delta.totalCostUsd,
4290
+ modelUsage: Object.fromEntries(modelUsageEntries),
4291
+ modelCostUsd: Object.fromEntries(delta.modelUsage.map((entry) => [entry.model, entry.costUsd])),
4292
+ };
4293
+ }
4294
+ else if (colorArgs !== undefined) {
4120
4295
  if (!service.recordColorUsage) {
4121
4296
  throw new Error('Session color is unavailable.');
4122
4297
  }
@@ -4169,20 +4344,22 @@ async function execute(argv, io, dependencies, signal) {
4169
4344
  return false;
4170
4345
  }
4171
4346
  activeSessionId = result.sessionId;
4347
+ await ensureCostBaseline(activeSessionId);
4348
+ const localCommand = colorArgs !== undefined || costTurn;
4172
4349
  if (streamOutput) {
4173
- if (colorArgs !== undefined) {
4350
+ if (localCommand) {
4174
4351
  streamOutput.syntheticAssistant(result.text);
4175
4352
  }
4176
4353
  streamOutput.result(result, startedAt);
4177
4354
  }
4178
4355
  else if (outputFormat === 'json') {
4179
4356
  const resultRuntimeInfo = service.runtimeInfo?.() ?? runtimeInfo;
4180
- writeJson(io, createSuccessResult(result, resultRuntimeInfo, startedAt, colorArgs !== undefined ? 0 : Math.max(1, jsonModelTurns)));
4357
+ writeJson(io, createSuccessResult(result, resultRuntimeInfo, startedAt, localCommand ? 0 : Math.max(1, jsonModelTurns)));
4181
4358
  }
4182
4359
  else if (outputFormat !== 'text')
4183
4360
  writeJson(io, { type: 'result', ...result });
4184
4361
  else
4185
- io.stdout(colorArgs !== undefined ? `${result.text}\n` : '\n');
4362
+ io.stdout(localCommand ? `${result.text}\n` : '\n');
4186
4363
  if (streamOutput && invocation.promptSuggestions) {
4187
4364
  try {
4188
4365
  const suggestion = await service.promptSuggestion?.(activeSessionId, runSignal);
@@ -9,6 +9,8 @@ export interface CompactionResult {
9
9
  summary: string;
10
10
  usage: ModelUsage;
11
11
  durationMs: number;
12
+ durationWithoutRetriesMs?: number;
13
+ model?: string;
12
14
  }
13
15
  export interface Compactor {
14
16
  compact(request: CompactionRequest): Promise<CompactionResult>;
@@ -66,7 +66,13 @@ export interface ModelUsage {
66
66
  outputTokens: number;
67
67
  cacheReadInputTokens?: number;
68
68
  cacheCreationInputTokens?: number;
69
+ webSearchRequests?: number;
70
+ /** Positive safe integer when the completing model's context window is known. */
71
+ contextWindow?: number;
72
+ /** Positive safe integer when the completing model's max output tokens are known. */
73
+ maxOutputTokens?: number;
69
74
  }
75
+ export type ModelUsageByModel = Readonly<Record<string, ModelUsage>>;
70
76
  export interface ModelWebSearch {
71
77
  allowedDomains?: readonly string[];
72
78
  blockedDomains?: readonly string[];
@@ -103,6 +109,9 @@ export type ModelStreamEvent = {
103
109
  retryDelayMs: number;
104
110
  errorStatus: number | null;
105
111
  error: ProviderErrorKind;
112
+ } | {
113
+ type: 'api-attempt-duration';
114
+ durationMs: number;
106
115
  };
107
116
  export interface ModelRequest {
108
117
  messages: readonly ModelMessage[];
@@ -125,6 +134,7 @@ export interface ModelProviderCapabilities {
125
134
  maxTokens: boolean;
126
135
  };
127
136
  contextWindowTokens?: number;
137
+ maxOutputTokens?: number;
128
138
  }
129
139
  export interface ModelProvider {
130
140
  readonly capabilities: ModelProviderCapabilities;
@@ -276,12 +286,17 @@ export interface ToolExecutionResult {
276
286
  images?: readonly ModelImage[];
277
287
  documents?: readonly ModelDocument[];
278
288
  isError: boolean;
289
+ linesAdded?: number;
290
+ linesRemoved?: number;
279
291
  usage?: ModelUsage;
292
+ modelUsage?: ModelUsageByModel;
280
293
  accessedPaths?: readonly string[];
281
294
  followUpUserMessages?: readonly string[];
282
295
  nativeToolUseResult?: Record<string, unknown>;
283
296
  nativeMcpMeta?: Record<string, unknown>;
284
297
  durationApiMs?: number;
298
+ durationApiWithoutRetriesMs?: number;
299
+ durationToolMs?: number;
285
300
  processOutput?: {
286
301
  stdout: string;
287
302
  stderr: string;
@@ -389,6 +404,14 @@ export interface AgentRunObserver {
389
404
  toolCompleted(call: ModelToolCall, result: ToolExecutionResult): Promise<void>;
390
405
  followUpUserMessagesCompleted?(messages: readonly string[]): Promise<void>;
391
406
  }
407
+ export interface ToolUseSummaryOutcome {
408
+ summary: string | null;
409
+ usage: ModelUsage;
410
+ modelUsage?: ModelUsageByModel;
411
+ durationApiMs: number;
412
+ durationApiWithoutRetriesMs: number;
413
+ meteredExternally: boolean;
414
+ }
392
415
  export interface AgentRuntimeOptions {
393
416
  tools?: ToolRegistry;
394
417
  permissions?: PermissionResolver;
@@ -407,7 +430,7 @@ export interface AgentRuntimeOptions {
407
430
  }[];
408
431
  lastAssistantText?: string;
409
432
  signal: AbortSignal;
410
- }) => Promise<string | null>;
433
+ }) => Promise<ToolUseSummaryOutcome | null>;
411
434
  }
412
435
  export interface AgentRunRequest {
413
436
  messages: readonly ModelMessage[];
@@ -421,6 +444,9 @@ export interface AgentRunRequest {
421
444
  onStop?: (text: string) => Promise<readonly string[] | {
422
445
  messages: readonly string[];
423
446
  usage?: ModelUsage;
447
+ modelUsage?: ModelUsageByModel;
448
+ durationApiMs?: number;
449
+ durationApiWithoutRetriesMs?: number;
424
450
  }>;
425
451
  signal?: AbortSignal;
426
452
  effort?: string;
@@ -436,7 +462,18 @@ export interface AgentToolRecoveryRequest extends Pick<AgentRunRequest, 'approve
436
462
  export interface AgentRunResult {
437
463
  text: string;
438
464
  usage: ModelUsage;
465
+ modelUsage?: ModelUsageByModel;
439
466
  durationApiMs?: number;
467
+ durationApiWithoutRetriesMs?: number;
468
+ durationToolMs?: number;
469
+ linesAdded?: number;
470
+ linesRemoved?: number;
471
+ /** Model rows the session tracker still must record after at least one
472
+ * externally metered summary outcome; undefined when every summary outcome
473
+ * is still unrecorded (so inclusive fields stay authoritative). */
474
+ unrecordedModelUsage?: ModelUsageByModel;
475
+ unrecordedDurationApiMs?: number;
476
+ unrecordedDurationApiWithoutRetriesMs?: number;
440
477
  }
441
478
  export declare class ModelProviderError extends Error {
442
479
  readonly name = "ModelProviderError";