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.
- package/README.md +11 -5
- package/dist/application/background-agent-manager.d.ts +7 -1
- package/dist/application/background-agent-manager.js +148 -5
- package/dist/application/metered-model-completion.d.ts +15 -0
- package/dist/application/metered-model-completion.js +76 -0
- package/dist/application/model-compactor.js +25 -1
- package/dist/application/session-cost-tracker.d.ts +50 -0
- package/dist/application/session-cost-tracker.js +254 -0
- package/dist/application/session-service.d.ts +13 -0
- package/dist/application/session-service.js +594 -69
- package/dist/application/subagent-service.d.ts +11 -1
- package/dist/application/subagent-service.js +41 -1
- package/dist/application/tool-use-summary.d.ts +7 -2
- package/dist/application/tool-use-summary.js +42 -10
- package/dist/application/workflow-manager.d.ts +9 -4
- package/dist/application/workflow-manager.js +151 -7
- package/dist/cli/interactive.d.ts +5 -1
- package/dist/cli/interactive.js +227 -15
- package/dist/cli/protocol.d.ts +11 -0
- package/dist/cli/protocol.js +16 -1
- package/dist/cli/tui/claude-command-inventory.d.ts +27 -27
- package/dist/cli/tui/claude-command-inventory.js +26 -26
- package/dist/cli/tui/config-dashboard.d.ts +2 -9
- package/dist/cli/tui/config-dashboard.js +6 -6
- package/dist/cli/tui/cost-summary.d.ts +34 -0
- package/dist/cli/tui/cost-summary.js +192 -0
- package/dist/cli/tui/doctor-dashboard.d.ts +15 -0
- package/dist/cli/tui/doctor-dashboard.js +81 -0
- package/dist/cli/tui/slash-commands.js +10 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +192 -15
- package/dist/core/compaction.d.ts +2 -0
- package/dist/core/runtime.d.ts +38 -1
- package/dist/core/runtime.js +265 -11
- package/dist/persistence/claude-cost-state-store.d.ts +48 -0
- package/dist/persistence/claude-cost-state-store.js +335 -0
- package/dist/providers/anthropic-compatible.js +34 -9
- package/dist/providers/fallback-provider.d.ts +2 -1
- package/dist/providers/fallback-provider.js +28 -3
- package/dist/tools/line-changes.d.ts +8 -0
- package/dist/tools/line-changes.js +35 -0
- package/dist/tools/local-tools.js +30 -1
- package/package.json +2 -1
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { type ClaudeSidechainPermissionMode } from '../compatibility/claude/sidechain.js';
|
|
2
2
|
import { type ContextAssembler } from '../core/context.js';
|
|
3
|
-
import { type ModelProvider, type ModelToolCall, type ModelToolDefinition, type ModelUsage, type PermissionResolver, type PermissionApproval, type PermissionDecision, type PermissionUpdate, type RuntimeEventSink, type ToolExecutionContext, type ToolExecutionResult, type ToolRegistry } from '../core/runtime.js';
|
|
3
|
+
import { type ModelProvider, type ModelToolCall, type ModelToolDefinition, type ModelUsage, type ModelUsageByModel, type PermissionResolver, type PermissionApproval, type PermissionDecision, type PermissionUpdate, type RuntimeEventSink, type ToolExecutionContext, type ToolExecutionResult, type ToolRegistry } from '../core/runtime.js';
|
|
4
4
|
import { type ClaudeAgentRuntimeDefinition, type ClaudeExtensionCatalog } from '../extensions/claude-extensions.js';
|
|
5
5
|
import type { ClaudeHookRunner } from '../hooks/claude-hooks.js';
|
|
6
6
|
import type { ClaudeMcpRuntime } from '../mcp/claude-mcp-tools.js';
|
|
@@ -23,6 +23,9 @@ export interface WorkflowAgentRunOptions {
|
|
|
23
23
|
export interface WorkflowAgentRunResult {
|
|
24
24
|
result: unknown;
|
|
25
25
|
usage: ModelUsage;
|
|
26
|
+
modelUsage?: ModelUsageByModel;
|
|
27
|
+
durationApiMs?: number;
|
|
28
|
+
durationApiWithoutRetriesMs?: number;
|
|
26
29
|
toolUseCount: number;
|
|
27
30
|
durationMs: number;
|
|
28
31
|
resolvedModel: string;
|
|
@@ -71,6 +74,10 @@ export interface ClaudeSubagentExecutorOptions {
|
|
|
71
74
|
toolNames?: readonly string[];
|
|
72
75
|
backgroundTaskNotifications?: (waitForRunning: boolean) => Promise<string[]>;
|
|
73
76
|
persistence?: 'disk' | 'memory';
|
|
77
|
+
onLineChanges?: (changes: {
|
|
78
|
+
readonly linesAdded: number;
|
|
79
|
+
readonly linesRemoved: number;
|
|
80
|
+
}) => void | Promise<void>;
|
|
74
81
|
}
|
|
75
82
|
export declare class ClaudeSubagentExecutor {
|
|
76
83
|
private readonly options;
|
|
@@ -100,6 +107,9 @@ export declare class ClaudeSubagentExecutor {
|
|
|
100
107
|
notifications(waitForRunning: boolean): Promise<{
|
|
101
108
|
messages: string[];
|
|
102
109
|
usage: ModelUsage;
|
|
110
|
+
modelUsage?: ModelUsageByModel;
|
|
111
|
+
durationApiMs?: number;
|
|
112
|
+
durationApiWithoutRetriesMs?: number;
|
|
103
113
|
}>;
|
|
104
114
|
private hydrateCompletedTask;
|
|
105
115
|
private resolvePersistedAgentId;
|
|
@@ -673,6 +673,15 @@ export class ClaudeSubagentExecutor {
|
|
|
673
673
|
].join('\n\n'),
|
|
674
674
|
isError: false,
|
|
675
675
|
usage: result.usage,
|
|
676
|
+
...(result.modelUsage ? { modelUsage: { ...result.modelUsage } } : {}),
|
|
677
|
+
...(result.durationApiMs === undefined
|
|
678
|
+
? {}
|
|
679
|
+
: { durationApiMs: result.durationApiMs }),
|
|
680
|
+
...(result.durationApiWithoutRetriesMs === undefined
|
|
681
|
+
? {}
|
|
682
|
+
: {
|
|
683
|
+
durationApiWithoutRetriesMs: result.durationApiWithoutRetriesMs,
|
|
684
|
+
}),
|
|
676
685
|
nativeToolUseResult: {
|
|
677
686
|
...createClaudeAgentToolUseResult({
|
|
678
687
|
prompt: input.prompt,
|
|
@@ -863,6 +872,15 @@ export class ClaudeSubagentExecutor {
|
|
|
863
872
|
return {
|
|
864
873
|
result: options.schema ? structured.value : run.text,
|
|
865
874
|
usage: run.usage,
|
|
875
|
+
...(run.modelUsage ? { modelUsage: { ...run.modelUsage } } : {}),
|
|
876
|
+
...(run.durationApiMs === undefined
|
|
877
|
+
? {}
|
|
878
|
+
: { durationApiMs: run.durationApiMs }),
|
|
879
|
+
...(run.durationApiWithoutRetriesMs === undefined
|
|
880
|
+
? {}
|
|
881
|
+
: {
|
|
882
|
+
durationApiWithoutRetriesMs: run.durationApiWithoutRetriesMs,
|
|
883
|
+
}),
|
|
866
884
|
toolUseCount: run.toolUseCount,
|
|
867
885
|
durationMs: Date.now() - startedAt,
|
|
868
886
|
resolvedModel: provider.model ?? 'praxis/provider',
|
|
@@ -1253,6 +1271,13 @@ export class ClaudeSubagentExecutor {
|
|
|
1253
1271
|
await append(toClaudeSidechainEntry(entry, options.agentId, options.input.subagentType));
|
|
1254
1272
|
},
|
|
1255
1273
|
toolCompleted: async (call, result) => {
|
|
1274
|
+
if (result.isError === false) {
|
|
1275
|
+
const linesAdded = result.linesAdded ?? 0;
|
|
1276
|
+
const linesRemoved = result.linesRemoved ?? 0;
|
|
1277
|
+
if (linesAdded !== 0 || linesRemoved !== 0) {
|
|
1278
|
+
await this.options.onLineChanges?.({ linesAdded, linesRemoved });
|
|
1279
|
+
}
|
|
1280
|
+
}
|
|
1256
1281
|
const nestedToolUseCount = result.nativeToolUseResult?.totalToolUseCount;
|
|
1257
1282
|
if (call.name === 'Agent' &&
|
|
1258
1283
|
typeof nestedToolUseCount === 'number' &&
|
|
@@ -1357,6 +1382,7 @@ export class ClaudeSubagentExecutor {
|
|
|
1357
1382
|
const effectiveEffort = options.effort ?? configuredEffort;
|
|
1358
1383
|
const result = await runtime.run({
|
|
1359
1384
|
messages: await assembleMessages(),
|
|
1385
|
+
collectMetrics: true,
|
|
1360
1386
|
reloadMessages: assembleMessages,
|
|
1361
1387
|
cwd,
|
|
1362
1388
|
...(effectiveEffort ? { effort: effectiveEffort } : {}),
|
|
@@ -1395,7 +1421,21 @@ export class ClaudeSubagentExecutor {
|
|
|
1395
1421
|
});
|
|
1396
1422
|
messages.push(...background.messages);
|
|
1397
1423
|
messages.push(...((await this.options.backgroundTaskNotifications?.(true)) ?? []));
|
|
1398
|
-
return {
|
|
1424
|
+
return {
|
|
1425
|
+
messages,
|
|
1426
|
+
usage: background.usage,
|
|
1427
|
+
...(background.modelUsage
|
|
1428
|
+
? { modelUsage: background.modelUsage }
|
|
1429
|
+
: {}),
|
|
1430
|
+
...(background.durationApiMs === undefined
|
|
1431
|
+
? {}
|
|
1432
|
+
: { durationApiMs: background.durationApiMs }),
|
|
1433
|
+
...(background.durationApiWithoutRetriesMs === undefined
|
|
1434
|
+
? {}
|
|
1435
|
+
: {
|
|
1436
|
+
durationApiWithoutRetriesMs: background.durationApiWithoutRetriesMs,
|
|
1437
|
+
}),
|
|
1438
|
+
};
|
|
1399
1439
|
},
|
|
1400
1440
|
}
|
|
1401
1441
|
: {}),
|
|
@@ -1,8 +1,13 @@
|
|
|
1
|
-
import type { ModelProvider } from '../core/runtime.js';
|
|
1
|
+
import type { ModelProvider, ModelUsage, ToolUseSummaryOutcome } from '../core/runtime.js';
|
|
2
2
|
export interface ToolUseSummaryInput {
|
|
3
3
|
name: string;
|
|
4
4
|
input: Record<string, unknown>;
|
|
5
5
|
output: string;
|
|
6
6
|
}
|
|
7
|
-
export declare function generateToolUseSummary(provider: ModelProvider, tools: readonly ToolUseSummaryInput[], signal: AbortSignal, lastAssistantText?: string
|
|
7
|
+
export declare function generateToolUseSummary(provider: ModelProvider, tools: readonly ToolUseSummaryInput[], signal: AbortSignal, lastAssistantText?: string, onMetrics?: (metrics: {
|
|
8
|
+
usage: ModelUsage;
|
|
9
|
+
model?: string;
|
|
10
|
+
durationApiMs: number;
|
|
11
|
+
durationApiWithoutRetriesMs: number;
|
|
12
|
+
}) => void): Promise<ToolUseSummaryOutcome | null>;
|
|
8
13
|
//# sourceMappingURL=tool-use-summary.d.ts.map
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { completeMeteredModelRequest, } from './metered-model-completion.js';
|
|
1
2
|
const TOOL_USE_SUMMARY_SYSTEM_PROMPT = `Write a short summary label describing what these tool calls accomplished. It appears as a single-line row in a mobile app and truncates around 30 characters, so think git-commit-subject, not sentence.
|
|
2
3
|
|
|
3
4
|
Keep the verb in past tense and the most distinctive noun. Drop articles, connectors, and long location context first.
|
|
@@ -20,7 +21,14 @@ function truncateJson(value, maxLength) {
|
|
|
20
21
|
return serialized;
|
|
21
22
|
return `${serialized.slice(0, maxLength - 3)}...`;
|
|
22
23
|
}
|
|
23
|
-
|
|
24
|
+
function hasNonZeroUsage(usage) {
|
|
25
|
+
return (usage.inputTokens > 0 ||
|
|
26
|
+
usage.outputTokens > 0 ||
|
|
27
|
+
(usage.cacheReadInputTokens ?? 0) > 0 ||
|
|
28
|
+
(usage.cacheCreationInputTokens ?? 0) > 0 ||
|
|
29
|
+
(usage.webSearchRequests ?? 0) > 0);
|
|
30
|
+
}
|
|
31
|
+
export async function generateToolUseSummary(provider, tools, signal, lastAssistantText, onMetrics) {
|
|
24
32
|
if (tools.length === 0 || signal.aborted)
|
|
25
33
|
return null;
|
|
26
34
|
const toolSummaries = tools
|
|
@@ -36,18 +44,42 @@ export async function generateToolUseSummary(provider, tools, signal, lastAssist
|
|
|
36
44
|
content: `${contextPrefix}Tools completed:\n\n${toolSummaries}\n\nLabel:`,
|
|
37
45
|
},
|
|
38
46
|
];
|
|
39
|
-
let
|
|
47
|
+
let metrics;
|
|
48
|
+
let summary = null;
|
|
40
49
|
try {
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
50
|
+
const completed = await completeMeteredModelRequest(provider, { messages, signal }, {
|
|
51
|
+
onMetrics: (recorded) => {
|
|
52
|
+
metrics = recorded;
|
|
53
|
+
},
|
|
54
|
+
});
|
|
55
|
+
summary =
|
|
56
|
+
completed.toolCalls.length === 0 ? completed.text.trim() || null : null;
|
|
47
57
|
}
|
|
48
58
|
catch {
|
|
49
|
-
|
|
59
|
+
// Provider/validation/abort errors keep the captured metrics but yield no
|
|
60
|
+
// summary, preserving the auxiliary-failure semantics.
|
|
61
|
+
summary = null;
|
|
62
|
+
}
|
|
63
|
+
// Invoke the external callback exactly once when the provider call started.
|
|
64
|
+
// Callback/tracker errors are intentionally not caught so cost accounting
|
|
65
|
+
// fails closed. Without an external callback nothing was committed, so the
|
|
66
|
+
// outcome stays unrecorded.
|
|
67
|
+
let meteredExternally = false;
|
|
68
|
+
if (metrics) {
|
|
69
|
+
onMetrics?.(metrics);
|
|
70
|
+
meteredExternally = onMetrics !== undefined;
|
|
50
71
|
}
|
|
51
|
-
|
|
72
|
+
const usage = metrics?.usage ?? { inputTokens: 0, outputTokens: 0 };
|
|
73
|
+
const model = metrics?.model !== undefined && metrics.model.trim() !== ''
|
|
74
|
+
? metrics.model
|
|
75
|
+
: 'praxis/provider';
|
|
76
|
+
return {
|
|
77
|
+
summary,
|
|
78
|
+
usage,
|
|
79
|
+
...(hasNonZeroUsage(usage) ? { modelUsage: { [model]: usage } } : {}),
|
|
80
|
+
durationApiMs: metrics?.durationApiMs ?? 0,
|
|
81
|
+
durationApiWithoutRetriesMs: metrics?.durationApiWithoutRetriesMs ?? 0,
|
|
82
|
+
meteredExternally,
|
|
83
|
+
};
|
|
52
84
|
}
|
|
53
85
|
//# sourceMappingURL=tool-use-summary.js.map
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { ModelUsage, ModelUsageByModel } from '../core/runtime.js';
|
|
1
2
|
import { ClaudeWorkflowStore } from '../persistence/claude-workflow-store.js';
|
|
2
3
|
import type { WorkflowAgentRunOptions, WorkflowAgentRunResult } from './subagent-service.js';
|
|
3
4
|
import { parseWorkflowScript, type ParsedWorkflowScript, type WorkflowMeta } from './workflow-meta.js';
|
|
@@ -74,6 +75,10 @@ interface WorkflowTask {
|
|
|
74
75
|
totalTokens: number;
|
|
75
76
|
totalInputTokens: number;
|
|
76
77
|
totalToolCalls: number;
|
|
78
|
+
usage: ModelUsage;
|
|
79
|
+
modelUsage: Map<string, ModelUsage>;
|
|
80
|
+
durationApiMs: number;
|
|
81
|
+
durationApiWithoutRetriesMs: number;
|
|
77
82
|
controller: AbortController;
|
|
78
83
|
promise: Promise<void>;
|
|
79
84
|
notificationPending: boolean;
|
|
@@ -107,10 +112,10 @@ export declare class WorkflowManager {
|
|
|
107
112
|
stopAndWait(taskId: string): Promise<void>;
|
|
108
113
|
notifications(waitForRunning: boolean): Promise<{
|
|
109
114
|
messages: string[];
|
|
110
|
-
usage:
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
115
|
+
usage: ModelUsage;
|
|
116
|
+
modelUsage?: ModelUsageByModel;
|
|
117
|
+
durationApiMs?: number;
|
|
118
|
+
durationApiWithoutRetriesMs?: number;
|
|
114
119
|
}>;
|
|
115
120
|
list(sessionId?: string): readonly WorkflowTaskSnapshot[];
|
|
116
121
|
abortAll(): void;
|
|
@@ -81,6 +81,117 @@ function validateAgentOptions(options) {
|
|
|
81
81
|
throw new Error('agent schema must be an object');
|
|
82
82
|
}
|
|
83
83
|
}
|
|
84
|
+
const workflowUsageCounterFields = [
|
|
85
|
+
'inputTokens',
|
|
86
|
+
'outputTokens',
|
|
87
|
+
'cacheReadInputTokens',
|
|
88
|
+
'cacheCreationInputTokens',
|
|
89
|
+
'webSearchRequests',
|
|
90
|
+
];
|
|
91
|
+
const workflowUsageMetadataFields = [
|
|
92
|
+
'contextWindow',
|
|
93
|
+
'maxOutputTokens',
|
|
94
|
+
];
|
|
95
|
+
function assertValidWorkflowUsage(usage) {
|
|
96
|
+
for (const field of workflowUsageCounterFields) {
|
|
97
|
+
const value = usage[field];
|
|
98
|
+
if (value !== undefined && (!Number.isSafeInteger(value) || value < 0)) {
|
|
99
|
+
throw new Error(`Workflow agent usage has an invalid ${field} counter`);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
function addWorkflowApiDuration(value, total, field) {
|
|
104
|
+
if (!Number.isFinite(value) || value < 0) {
|
|
105
|
+
throw new TypeError(`Workflow ${field} must be a finite nonnegative number`);
|
|
106
|
+
}
|
|
107
|
+
const next = total + value;
|
|
108
|
+
if (!Number.isFinite(next) || next < 0) {
|
|
109
|
+
throw new TypeError(`Workflow ${field} total overflow`);
|
|
110
|
+
}
|
|
111
|
+
return next;
|
|
112
|
+
}
|
|
113
|
+
function addWorkflowUsageChecked(model, left, right) {
|
|
114
|
+
assertValidWorkflowUsage(right);
|
|
115
|
+
const inputTokens = left.inputTokens + right.inputTokens;
|
|
116
|
+
const outputTokens = left.outputTokens + right.outputTokens;
|
|
117
|
+
const cacheReadInputTokens = (left.cacheReadInputTokens ?? 0) + (right.cacheReadInputTokens ?? 0);
|
|
118
|
+
const cacheCreationInputTokens = (left.cacheCreationInputTokens ?? 0) + (right.cacheCreationInputTokens ?? 0);
|
|
119
|
+
const webSearchRequests = (left.webSearchRequests ?? 0) + (right.webSearchRequests ?? 0);
|
|
120
|
+
if (!Number.isSafeInteger(inputTokens) ||
|
|
121
|
+
!Number.isSafeInteger(outputTokens) ||
|
|
122
|
+
!Number.isSafeInteger(cacheReadInputTokens) ||
|
|
123
|
+
!Number.isSafeInteger(cacheCreationInputTokens) ||
|
|
124
|
+
!Number.isSafeInteger(webSearchRequests)) {
|
|
125
|
+
throw new Error('Workflow model usage total overflow');
|
|
126
|
+
}
|
|
127
|
+
// Aggregates without a model stay counter-only; per-model rows merge their
|
|
128
|
+
// capability metadata with conflict rejection.
|
|
129
|
+
const metadata = model === undefined ? {} : mergeWorkflowUsageMetadata(model, left, right);
|
|
130
|
+
return {
|
|
131
|
+
inputTokens,
|
|
132
|
+
outputTokens,
|
|
133
|
+
...(cacheReadInputTokens === 0 ? {} : { cacheReadInputTokens }),
|
|
134
|
+
...(cacheCreationInputTokens === 0 ? {} : { cacheCreationInputTokens }),
|
|
135
|
+
...(webSearchRequests === 0 ? {} : { webSearchRequests }),
|
|
136
|
+
...metadata,
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
function mergeWorkflowUsageMetadata(model, left, right) {
|
|
140
|
+
const contextWindow = mergeWorkflowUsageMetadataField(model, 'contextWindow', left.contextWindow, right.contextWindow);
|
|
141
|
+
const maxOutputTokens = mergeWorkflowUsageMetadataField(model, 'maxOutputTokens', left.maxOutputTokens, right.maxOutputTokens);
|
|
142
|
+
return {
|
|
143
|
+
...(contextWindow === undefined ? {} : { contextWindow }),
|
|
144
|
+
...(maxOutputTokens === undefined ? {} : { maxOutputTokens }),
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
function mergeWorkflowUsageMetadataField(model, field, left, right) {
|
|
148
|
+
if (left === undefined)
|
|
149
|
+
return right;
|
|
150
|
+
if (right === undefined)
|
|
151
|
+
return left;
|
|
152
|
+
if (left !== right) {
|
|
153
|
+
throw new Error(`Workflow model usage for "${model}" has conflicting ${field} values: ${left} vs ${right}`);
|
|
154
|
+
}
|
|
155
|
+
return left;
|
|
156
|
+
}
|
|
157
|
+
function assertValidWorkflowModelUsageEntry(model, usage) {
|
|
158
|
+
if (model.trim() === '') {
|
|
159
|
+
throw new Error('Workflow model usage breakdown contains a blank model name');
|
|
160
|
+
}
|
|
161
|
+
assertValidWorkflowUsage(usage);
|
|
162
|
+
for (const field of workflowUsageMetadataFields) {
|
|
163
|
+
const value = usage[field];
|
|
164
|
+
if (value !== undefined && (!Number.isSafeInteger(value) || value < 1)) {
|
|
165
|
+
throw new Error(`Workflow model usage for "${model}" has an invalid ${field} metadata value`);
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
function mergeWorkflowModelUsageEntry(map, model, usage) {
|
|
170
|
+
assertValidWorkflowModelUsageEntry(model, usage);
|
|
171
|
+
const existing = map.get(model);
|
|
172
|
+
if (existing === undefined) {
|
|
173
|
+
map.set(model, { ...usage });
|
|
174
|
+
return;
|
|
175
|
+
}
|
|
176
|
+
map.set(model, addWorkflowUsageChecked(model, existing, usage));
|
|
177
|
+
}
|
|
178
|
+
function mergeWorkflowModelUsage(map, breakdown) {
|
|
179
|
+
const entries = Object.entries(breakdown);
|
|
180
|
+
if (entries.length === 0)
|
|
181
|
+
return;
|
|
182
|
+
// Validate every key, counter, metadata, and merged sum before adding any
|
|
183
|
+
// entry from this agent result so a malformed or conflicting breakdown never
|
|
184
|
+
// merges partially.
|
|
185
|
+
for (const [model, usage] of entries) {
|
|
186
|
+
assertValidWorkflowModelUsageEntry(model, usage);
|
|
187
|
+
const existing = map.get(model);
|
|
188
|
+
if (existing !== undefined)
|
|
189
|
+
addWorkflowUsageChecked(model, existing, usage);
|
|
190
|
+
}
|
|
191
|
+
for (const [model, usage] of entries) {
|
|
192
|
+
mergeWorkflowModelUsageEntry(map, model, usage);
|
|
193
|
+
}
|
|
194
|
+
}
|
|
84
195
|
export class WorkflowManager {
|
|
85
196
|
configRoot;
|
|
86
197
|
cwd;
|
|
@@ -140,6 +251,10 @@ export class WorkflowManager {
|
|
|
140
251
|
totalTokens: 0,
|
|
141
252
|
totalInputTokens: 0,
|
|
142
253
|
totalToolCalls: 0,
|
|
254
|
+
usage: { inputTokens: 0, outputTokens: 0 },
|
|
255
|
+
modelUsage: new Map(),
|
|
256
|
+
durationApiMs: 0,
|
|
257
|
+
durationApiWithoutRetriesMs: 0,
|
|
143
258
|
controller,
|
|
144
259
|
promise: Promise.resolve(),
|
|
145
260
|
notificationPending: false,
|
|
@@ -219,20 +334,39 @@ export class WorkflowManager {
|
|
|
219
334
|
.map(({ promise }) => promise));
|
|
220
335
|
}
|
|
221
336
|
const messages = [];
|
|
222
|
-
let
|
|
223
|
-
|
|
337
|
+
let usage = { inputTokens: 0, outputTokens: 0 };
|
|
338
|
+
const modelUsage = new Map();
|
|
339
|
+
let durationApiMs = 0;
|
|
340
|
+
let durationApiWithoutRetriesMs = 0;
|
|
341
|
+
let durationSeen = false;
|
|
342
|
+
const consumedTasks = [];
|
|
224
343
|
for (const task of this.tasks.values()) {
|
|
225
344
|
if (task.status === 'running' || !task.notificationPending)
|
|
226
345
|
continue;
|
|
227
|
-
task.notificationPending = false;
|
|
228
346
|
messages.push(this.notification(task));
|
|
229
|
-
|
|
230
|
-
|
|
347
|
+
usage = addWorkflowUsageChecked(undefined, usage, task.usage);
|
|
348
|
+
for (const [model, entry] of task.modelUsage) {
|
|
349
|
+
mergeWorkflowModelUsageEntry(modelUsage, model, entry);
|
|
350
|
+
}
|
|
351
|
+
if (task.durationApiMs !== 0 || task.durationApiWithoutRetriesMs !== 0) {
|
|
352
|
+
durationSeen = true;
|
|
353
|
+
durationApiMs = addWorkflowApiDuration(task.durationApiMs, durationApiMs, 'durationApiMs');
|
|
354
|
+
durationApiWithoutRetriesMs = addWorkflowApiDuration(task.durationApiWithoutRetriesMs, durationApiWithoutRetriesMs, 'durationApiWithoutRetriesMs');
|
|
355
|
+
}
|
|
356
|
+
consumedTasks.push(task);
|
|
231
357
|
}
|
|
232
|
-
|
|
358
|
+
const modelUsageByModel = modelUsage.size === 0 ? undefined : Object.fromEntries(modelUsage);
|
|
359
|
+
const result = {
|
|
233
360
|
messages,
|
|
234
|
-
usage
|
|
361
|
+
usage,
|
|
362
|
+
...(modelUsageByModel === undefined
|
|
363
|
+
? {}
|
|
364
|
+
: { modelUsage: modelUsageByModel }),
|
|
365
|
+
...(durationSeen ? { durationApiMs, durationApiWithoutRetriesMs } : {}),
|
|
235
366
|
};
|
|
367
|
+
for (const task of consumedTasks)
|
|
368
|
+
task.notificationPending = false;
|
|
369
|
+
return result;
|
|
236
370
|
}
|
|
237
371
|
list(sessionId) {
|
|
238
372
|
return [...this.tasks.values()]
|
|
@@ -409,6 +543,16 @@ export class WorkflowManager {
|
|
|
409
543
|
task.totalTokens += progress.totalTokens;
|
|
410
544
|
task.totalInputTokens += result.usage.inputTokens;
|
|
411
545
|
task.totalToolCalls += result.toolUseCount;
|
|
546
|
+
task.usage = addWorkflowUsageChecked(undefined, task.usage, result.usage);
|
|
547
|
+
if (result.modelUsage !== undefined) {
|
|
548
|
+
mergeWorkflowModelUsage(task.modelUsage, result.modelUsage);
|
|
549
|
+
}
|
|
550
|
+
if (result.durationApiMs !== undefined ||
|
|
551
|
+
result.durationApiWithoutRetriesMs !== undefined) {
|
|
552
|
+
const total = result.durationApiMs ?? 0;
|
|
553
|
+
task.durationApiMs = addWorkflowApiDuration(total, task.durationApiMs, 'durationApiMs');
|
|
554
|
+
task.durationApiWithoutRetriesMs = addWorkflowApiDuration(result.durationApiWithoutRetriesMs ?? total, task.durationApiWithoutRetriesMs, 'durationApiWithoutRetriesMs');
|
|
555
|
+
}
|
|
412
556
|
await task.store.append({
|
|
413
557
|
type: 'result',
|
|
414
558
|
key,
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { ForkResult, ManualCompactResult, ManualCompactSelection, RewindPoint, SessionForkCheckpoint, SessionRunResult, SessionSummary, SideQuestionForkResult, SideQuestionResult } from '../application/session-service.js';
|
|
2
|
+
import type { ClaudeSessionCostSnapshot } from '../application/session-cost-tracker.js';
|
|
2
3
|
import type { ModelImage, ModelToolCall, PermissionApproval, PermissionDecision, RuntimeEventSink } from '../core/runtime.js';
|
|
3
4
|
import type { CliElicitationRequest, CliElicitationResult, CliRuntimeInfo } from './protocol.js';
|
|
4
5
|
import type { ClaudeInteractiveToolCallbacks } from '../tools/claude-interactive-tools.js';
|
|
@@ -13,6 +14,7 @@ import type { AgentColorName } from '../compatibility/claude/agent-color.js';
|
|
|
13
14
|
import type { ClaudeResourceScope } from '../compatibility/claude/shared-resources.js';
|
|
14
15
|
import type { TuiHookConfiguration } from './tui/hook-settings.js';
|
|
15
16
|
import { type TuiSlashCommand } from './tui/slash-commands.js';
|
|
17
|
+
import { type DoctorReport } from '../maintenance/doctor.js';
|
|
16
18
|
import { type TuiAgentEntry, type TuiFileEntry } from './tui/file-picker.js';
|
|
17
19
|
import { type TuiEditorOptions, type TuiEditorResult } from './tui/external-editor.js';
|
|
18
20
|
import { type TuiClipboardContent } from './tui/clipboard.js';
|
|
@@ -34,6 +36,7 @@ interface InteractiveSessionCommands {
|
|
|
34
36
|
fork(sessionId: string, targetSessionId?: string, resumeSessionAt?: string): Promise<ForkResult>;
|
|
35
37
|
sessions(): Promise<SessionSummary[]>;
|
|
36
38
|
transcript?(sessionId: string): Promise<TranscriptItem[]>;
|
|
39
|
+
costSnapshot?(sessionId: string): Promise<ClaudeSessionCostSnapshot>;
|
|
37
40
|
compact?(sessionId: string, signal?: AbortSignal, selection?: ManualCompactSelection): Promise<ManualCompactResult>;
|
|
38
41
|
rewindFiles?(sessionId: string, userMessageId: string): Promise<void>;
|
|
39
42
|
rewindPoints?(sessionId: string): Promise<RewindPoint[]>;
|
|
@@ -132,6 +135,7 @@ interface InteractiveAppProps {
|
|
|
132
135
|
allowDangerouslySkipPermissions?: boolean;
|
|
133
136
|
additionalDirectories?: readonly string[];
|
|
134
137
|
diffLoader?: () => Promise<TuiDiffSnapshot>;
|
|
138
|
+
doctorLoader?: () => Promise<DoctorReport>;
|
|
135
139
|
fileLoader?: () => Promise<readonly TuiFileEntry[]>;
|
|
136
140
|
externalEditor?: (prompt: string, options: TuiEditorOptions) => Promise<TuiEditorResult>;
|
|
137
141
|
keybindingsConfigRoot?: string;
|
|
@@ -184,7 +188,7 @@ interface InteractiveAppProps {
|
|
|
184
188
|
releaseNotesLoader?: (configRoot: string) => Promise<string>;
|
|
185
189
|
settingSources?: readonly ClaudeResourceScope[];
|
|
186
190
|
}
|
|
187
|
-
export declare function InteractiveApp({ factory, initialSessions, initialPrompt, initialHistory, initialSessionColor, signal, onCancel, onTurnChange, onCleanup, onRendererChange, onBackground, axScreenReader, allowNewSession, resume, display, terminalWidth, slashCommands, agents, allowDangerouslySkipPermissions, additionalDirectories, diffLoader, fileLoader, externalEditor, keybindingsConfigRoot, keybindingsFile, keybindingsLoader, keybindingsEditor, memoryFilesLoader, memoryEditor, memoryFolderOpener, suspendProcess, clipboardReader, clipboardWriter, sideQuestionClipboardWriter, exportWriter, permissionRuleStore, sandboxStore: suppliedSandboxStore, recentlyDeniedStore: suppliedRecentlyDeniedStore, terminalSetup: terminalSetupOverride, themeStore, initialThemeSettings, initialThemeLoadError, workspaceDirectoryResolver, workspaceDirectoryCompleter, runtimeSettings: suppliedRuntimeSettings, runtimeSettingsTarget, notificationWriter, elicitationUrlOpener, releaseNotesLoader, settingSources, }: InteractiveAppProps): import("react").JSX.Element;
|
|
191
|
+
export declare function InteractiveApp({ factory, initialSessions, initialPrompt, initialHistory, initialSessionColor, signal, onCancel, onTurnChange, onCleanup, onRendererChange, onBackground, axScreenReader, allowNewSession, resume, display, terminalWidth, slashCommands, agents, allowDangerouslySkipPermissions, additionalDirectories, diffLoader, doctorLoader, fileLoader, externalEditor, keybindingsConfigRoot, keybindingsFile, keybindingsLoader, keybindingsEditor, memoryFilesLoader, memoryEditor, memoryFolderOpener, suspendProcess, clipboardReader, clipboardWriter, sideQuestionClipboardWriter, exportWriter, permissionRuleStore, sandboxStore: suppliedSandboxStore, recentlyDeniedStore: suppliedRecentlyDeniedStore, terminalSetup: terminalSetupOverride, themeStore, initialThemeSettings, initialThemeLoadError, workspaceDirectoryResolver, workspaceDirectoryCompleter, runtimeSettings: suppliedRuntimeSettings, runtimeSettingsTarget, notificationWriter, elicitationUrlOpener, releaseNotesLoader, settingSources, }: InteractiveAppProps): import("react").JSX.Element;
|
|
188
192
|
export declare function runInteractive(options: {
|
|
189
193
|
factory: InteractiveServiceFactory;
|
|
190
194
|
initialPrompt?: string;
|