@xpufx/paseo-top 0.4.3 → 0.4.5
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/OVERVIEW.md +17 -0
- package/client/host-ui.tsx +184 -0
- package/client/multi-host-view.tsx +57 -43
- package/client/multi-host.ts +36 -13
- package/client/pill-labels.ts +124 -7
- package/client/pill.tsx +724 -669
- package/client/resources-query.ts +1 -1
- package/client/settings-events.ts +33 -0
- package/client/settings-ui.tsx +86 -9
- package/client/surface.tsx +310 -274
- package/client/telemetry-copy.ts +8 -2
- package/client/telemetry.tsx +372 -321
- package/client/turn-panel.tsx +126 -104
- package/client/vendor/paseo-plugin-helper/{host.ts → core/host.ts} +21 -61
- package/client/vendor/paseo-plugin-helper/core/index.ts +76 -0
- package/client/vendor/paseo-plugin-helper/core/plugin-load-count.ts +89 -0
- package/client/vendor/paseo-plugin-helper/{query-refresh.ts → core/query-refresh.ts} +2 -2
- package/client/vendor/paseo-plugin-helper/{query.ts → core/query.ts} +2 -2
- package/client/vendor/paseo-plugin-helper/{settings.ts → core/settings.ts} +1 -1
- package/client/vendor/paseo-plugin-helper/{shared-settings.ts → core/shared-settings.ts} +2 -2
- package/client/vendor/paseo-plugin-helper/{utils → lifecycle}/clipboard.ts +2 -2
- package/client/vendor/paseo-plugin-helper/lifecycle/flair.ts +26 -0
- package/client/vendor/paseo-plugin-helper/{theme/color-utils.ts → lifecycle/host-color.ts} +95 -38
- package/client/vendor/paseo-plugin-helper/lifecycle/host-theme.tsx +109 -0
- package/client/vendor/paseo-plugin-helper/lifecycle/index.ts +23 -0
- package/client/vendor/paseo-plugin-helper/lifecycle/multi-host.ts +66 -0
- package/client/vendor/paseo-plugin-helper/{panel.tsx → lifecycle/panel.tsx} +7 -7
- package/client/vendor/paseo-plugin-helper/{pill.tsx → lifecycle/pill.tsx} +118 -338
- package/client/vendor/paseo-plugin-helper/lifecycle/providers.tsx +46 -0
- package/client/vendor/paseo-plugin-helper/lifecycle/scroll-owner.ts +26 -0
- package/client/vendor/paseo-plugin-helper/lifecycle/surface.tsx +140 -0
- package/client/vendor/paseo-plugin-helper/ui/color.ts +1 -0
- package/client/vendor/paseo-plugin-helper/ui/content.tsx +1326 -0
- package/client/vendor/paseo-plugin-helper/ui/controls.tsx +1018 -0
- package/client/vendor/paseo-plugin-helper/{components/DataTable.tsx → ui/data.tsx} +55 -47
- package/client/vendor/paseo-plugin-helper/ui/index.ts +44 -0
- package/client/vendor/paseo-plugin-helper/ui/layout.tsx +263 -0
- package/client/vendor/paseo-plugin-helper/ui/modal.tsx +111 -0
- package/client/vendor/paseo-plugin-helper/ui/plugin-reload-banner.tsx +119 -0
- package/client/vendor/paseo-plugin-helper/{settings-screen.tsx → ui/settings-screen.tsx} +3 -3
- package/client/vendor/paseo-plugin-helper/ui/settings.ts +36 -0
- package/client/vendor/paseo-plugin-helper/ui/text.tsx +821 -0
- package/client/vendor/paseo-plugin-helper/ui/theme.tsx +1 -0
- package/client/vendor/permission-audit/client.tsx +706 -0
- package/client/vendor/permission-audit/index.tsx +1 -0
- package/index.client.tsx +58 -37
- package/index.server.ts +256 -227
- package/package.json +14 -2
- package/paseo-plugin.json +2 -0
- package/screenshots/1-paseo-top-modal-system.jpg +0 -0
- package/screenshots/2-paseo-top-modal-workspace.jpg +0 -0
- package/screenshots/3-paseo-top-modal-settings.jpg +0 -0
- package/screenshots/4-paseo-top-modal-about.jpg +0 -0
- package/screenshots/paseo-top-dashboard.jpg +0 -0
- package/screenshots/paseo-top-general-view.jpg +0 -0
- package/server/resources.ts +98 -0
- package/server/vendor/paseo-plugin-helper/custom-pills.ts +70 -0
- package/server/vendor/paseo-plugin-helper/index.ts +4 -0
- package/server/vendor/paseo-plugin-helper/logger.ts +5 -16
- package/server/vendor/paseo-plugin-helper/mcp-injection.ts +20 -2
- package/server/vendor/paseo-plugin-helper/plugins.ts +184 -19
- package/server/vendor/paseo-plugin-helper/rpc-guard.ts +2 -2
- package/server/vendor/paseo-plugin-helper/storage-guard.ts +294 -0
- package/server/vendor/paseo-plugin-helper/storage.ts +36 -2
- package/server/vendor/paseo-plugin-helper/telemetry.ts +486 -0
- package/server/vendor/paseo-plugin-helper/tickets.ts +1232 -0
- package/server/vendor/permission-audit/index.ts +1 -0
- package/server/vendor/permission-audit/server.ts +839 -0
- package/shared/helper-version.ts +20 -0
- package/shared/multi-host.ts +156 -155
- package/shared/resources.ts +146 -1
- package/shared/vendor/paseo-plugin-helper/async.ts +14 -7
- package/shared/vendor/paseo-plugin-helper/features.ts +244 -0
- package/shared/vendor/paseo-plugin-helper/forge.ts +27 -0
- package/shared/vendor/paseo-plugin-helper/index.ts +2 -0
- package/shared/vendor/paseo-plugin-helper/tickets.ts +1104 -0
- package/shared/vendor/permission-audit/filter.ts +61 -0
- package/shared/vendor/permission-audit/index.ts +2 -0
- package/shared/vendor/permission-audit/shared.ts +119 -0
- package/shared/version.ts +1 -1
- package/client/vendor/paseo-plugin-helper/components/AboutSection.tsx +0 -493
- package/client/vendor/paseo-plugin-helper/components/AttentionBeacon.tsx +0 -250
- package/client/vendor/paseo-plugin-helper/components/Badge.tsx +0 -156
- package/client/vendor/paseo-plugin-helper/components/Button.tsx +0 -184
- package/client/vendor/paseo-plugin-helper/components/Card.tsx +0 -225
- package/client/vendor/paseo-plugin-helper/components/CodeBlock.tsx +0 -196
- package/client/vendor/paseo-plugin-helper/components/Collapsible.tsx +0 -277
- package/client/vendor/paseo-plugin-helper/components/CommandBox.tsx +0 -172
- package/client/vendor/paseo-plugin-helper/components/CopyButton.tsx +0 -180
- package/client/vendor/paseo-plugin-helper/components/EmptyState.tsx +0 -97
- package/client/vendor/paseo-plugin-helper/components/HighlightedText.tsx +0 -70
- package/client/vendor/paseo-plugin-helper/components/InlineButton.tsx +0 -75
- package/client/vendor/paseo-plugin-helper/components/KeyValue.tsx +0 -446
- package/client/vendor/paseo-plugin-helper/components/MetricGauge.tsx +0 -247
- package/client/vendor/paseo-plugin-helper/components/ProgressBar.tsx +0 -117
- package/client/vendor/paseo-plugin-helper/components/Responsive.tsx +0 -53
- package/client/vendor/paseo-plugin-helper/components/SearchInput.tsx +0 -118
- package/client/vendor/paseo-plugin-helper/components/SectionHeader.tsx +0 -80
- package/client/vendor/paseo-plugin-helper/components/Select.tsx +0 -215
- package/client/vendor/paseo-plugin-helper/components/StatusDot.tsx +0 -80
- package/client/vendor/paseo-plugin-helper/components/Tabs.tsx +0 -319
- package/client/vendor/paseo-plugin-helper/components/TextInput.tsx +0 -150
- package/client/vendor/paseo-plugin-helper/components/Toggle.tsx +0 -163
- package/client/vendor/paseo-plugin-helper/components/TruncatedText.tsx +0 -157
- package/client/vendor/paseo-plugin-helper/components/index.ts +0 -25
- package/client/vendor/paseo-plugin-helper/custom-pills.tsx +0 -224
- package/client/vendor/paseo-plugin-helper/forge-icon.tsx +0 -79
- package/client/vendor/paseo-plugin-helper/icon.tsx +0 -39
- package/client/vendor/paseo-plugin-helper/index.ts +0 -29
- package/client/vendor/paseo-plugin-helper/layout/ActionBar.tsx +0 -49
- package/client/vendor/paseo-plugin-helper/layout/FormRow.tsx +0 -103
- package/client/vendor/paseo-plugin-helper/layout/Grid.tsx +0 -65
- package/client/vendor/paseo-plugin-helper/layout/ModalBody.tsx +0 -378
- package/client/vendor/paseo-plugin-helper/layout/ModalContent.tsx +0 -51
- package/client/vendor/paseo-plugin-helper/layout/Row.tsx +0 -39
- package/client/vendor/paseo-plugin-helper/layout/Stack.tsx +0 -39
- package/client/vendor/paseo-plugin-helper/layout/index.ts +0 -7
- package/client/vendor/paseo-plugin-helper/styles/index.ts +0 -1
- package/client/vendor/paseo-plugin-helper/styles/recipes.ts +0 -550
- package/client/vendor/paseo-plugin-helper/surface.tsx +0 -80
- package/client/vendor/paseo-plugin-helper/theme/flair.ts +0 -76
- package/client/vendor/paseo-plugin-helper/theme/host-variables.ts +0 -121
- package/client/vendor/paseo-plugin-helper/theme/index.ts +0 -7
- package/client/vendor/paseo-plugin-helper/theme/provider.tsx +0 -178
- package/client/vendor/paseo-plugin-helper/theme/responsive.ts +0 -213
- package/client/vendor/paseo-plugin-helper/theme/tokens.ts +0 -200
- package/client/vendor/paseo-plugin-helper/theme/useResponsive.ts +0 -57
- /package/client/vendor/paseo-plugin-helper/{command-center.ts → core/command-center.ts} +0 -0
- /package/client/vendor/paseo-plugin-helper/{snapshot.ts → core/snapshot.ts} +0 -0
- /package/client/vendor/paseo-plugin-helper/{utils → lifecycle}/haptics.ts +0 -0
|
@@ -0,0 +1,486 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import type { FeatureModule, FeatureDisposer } from "../../../shared/vendor/paseo-plugin-helper/features";
|
|
3
|
+
import type { PluginRpcContract } from "../../../shared/vendor/paseo-plugin-helper/rpc";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Raw usage structure as emitted by various Paseo agent engines and providers.
|
|
7
|
+
*/
|
|
8
|
+
export interface RawTurnUsage {
|
|
9
|
+
inputTokens?: number;
|
|
10
|
+
outputTokens?: number;
|
|
11
|
+
cachedTokens?: number;
|
|
12
|
+
cachedInputTokens?: number;
|
|
13
|
+
reasoningTokens?: number;
|
|
14
|
+
contextWindowUsedTokens?: number;
|
|
15
|
+
contextWindowMaxTokens?: number;
|
|
16
|
+
totalCostUsd?: number;
|
|
17
|
+
/** Claude Code SDK nested iterations array */
|
|
18
|
+
iterations?: Array<{
|
|
19
|
+
input_tokens?: number;
|
|
20
|
+
cache_creation_input_tokens?: number;
|
|
21
|
+
cache_read_input_tokens?: number;
|
|
22
|
+
output_tokens?: number;
|
|
23
|
+
}>;
|
|
24
|
+
/** OpenCode / ACP tokens object */
|
|
25
|
+
tokens?: {
|
|
26
|
+
input?: number;
|
|
27
|
+
output?: number;
|
|
28
|
+
reasoning?: number;
|
|
29
|
+
cache_read?: number;
|
|
30
|
+
};
|
|
31
|
+
[key: string]: unknown;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export interface RawTurnOutcome {
|
|
35
|
+
kind: "completed" | "failed" | "canceled" | string;
|
|
36
|
+
error?: { message: string; code?: string };
|
|
37
|
+
reason?: string;
|
|
38
|
+
stopReason?: string;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export interface RawTurnData {
|
|
42
|
+
turnId?: string | null;
|
|
43
|
+
agentId: string;
|
|
44
|
+
repo?: string | null;
|
|
45
|
+
role?: string | null;
|
|
46
|
+
model?: string | null;
|
|
47
|
+
provider?: string | null;
|
|
48
|
+
outcome?: RawTurnOutcome;
|
|
49
|
+
durationMs?: number;
|
|
50
|
+
timeline?: readonly unknown[];
|
|
51
|
+
liveUsage?: RawTurnUsage | null;
|
|
52
|
+
gitBefore?: { insertions: number; deletions: number; filesChanged: number } | null;
|
|
53
|
+
gitAfter?: { insertions: number; deletions: number; filesChanged: number } | null;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export const NormalizedTurnTelemetrySchema = z.object({
|
|
57
|
+
turnId: z.string().nullable(),
|
|
58
|
+
agentId: z.string(),
|
|
59
|
+
repo: z.string().nullable(),
|
|
60
|
+
role: z.string().nullable(),
|
|
61
|
+
model: z.string().nullable(),
|
|
62
|
+
provider: z.string().nullable(),
|
|
63
|
+
timestamp: z.string(),
|
|
64
|
+
durationMs: z.number(),
|
|
65
|
+
inputTokens: z.number(),
|
|
66
|
+
outputTokens: z.number(),
|
|
67
|
+
cachedTokens: z.number(),
|
|
68
|
+
reasoningTokens: z.number(),
|
|
69
|
+
totalTokens: z.number(),
|
|
70
|
+
tokensPerSecond: z.number(),
|
|
71
|
+
contextUsedTokens: z.number(),
|
|
72
|
+
contextMaxTokens: z.number(),
|
|
73
|
+
contextUtilizationPct: z.number(),
|
|
74
|
+
costUsd: z.number(),
|
|
75
|
+
toolCallsCount: z.number(),
|
|
76
|
+
toolErrorsCount: z.number(),
|
|
77
|
+
finishReason: z.string(),
|
|
78
|
+
stopReason: z.string().optional(),
|
|
79
|
+
errorMessage: z.string().optional(),
|
|
80
|
+
gitInsertions: z.number(),
|
|
81
|
+
gitDeletions: z.number(),
|
|
82
|
+
gitFilesChanged: z.number(),
|
|
83
|
+
isInterrupted: z.boolean(),
|
|
84
|
+
isSubscriptionZeroCost: z.boolean().optional(),
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
export type NormalizedTurnTelemetry = z.infer<typeof NormalizedTurnTelemetrySchema>;
|
|
88
|
+
|
|
89
|
+
function finite(val: unknown, fallback = 0): number {
|
|
90
|
+
return typeof val === "number" && Number.isFinite(val) ? val : fallback;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function round2(val: number): number {
|
|
94
|
+
return Math.round(val * 100) / 100;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Normalizes vendor/provider quirks into a canonical TurnTelemetry record:
|
|
99
|
+
* - Claude nested iterations array accumulation
|
|
100
|
+
* - OpenCode isolated reasoning tokens
|
|
101
|
+
* - ACP asynchronous context meter readings
|
|
102
|
+
* - Interrupted / canceled turn normalization (prevents division by zero)
|
|
103
|
+
* - Subscription $0 cost normalization
|
|
104
|
+
*/
|
|
105
|
+
export function normalizeTurnTelemetry(
|
|
106
|
+
raw: RawTurnData,
|
|
107
|
+
now = Date.now(),
|
|
108
|
+
): NormalizedTurnTelemetry {
|
|
109
|
+
const usage = raw.liveUsage;
|
|
110
|
+
let inputTokens = 0;
|
|
111
|
+
let outputTokens = 0;
|
|
112
|
+
let cachedTokens = 0;
|
|
113
|
+
let reasoningTokens = 0;
|
|
114
|
+
let contextUsedTokens = 0;
|
|
115
|
+
let contextMaxTokens = 0;
|
|
116
|
+
let costUsd = 0;
|
|
117
|
+
|
|
118
|
+
if (usage) {
|
|
119
|
+
if (Array.isArray(usage.iterations) && usage.iterations.length > 0) {
|
|
120
|
+
// Claude nested iterations normalization
|
|
121
|
+
let iterInput = 0;
|
|
122
|
+
let iterOutput = 0;
|
|
123
|
+
let iterCache = 0;
|
|
124
|
+
for (const iter of usage.iterations) {
|
|
125
|
+
iterInput += finite(iter.input_tokens);
|
|
126
|
+
iterCache += finite(iter.cache_read_input_tokens) + finite(iter.cache_creation_input_tokens);
|
|
127
|
+
iterOutput += finite(iter.output_tokens);
|
|
128
|
+
}
|
|
129
|
+
inputTokens = iterInput;
|
|
130
|
+
cachedTokens = iterCache;
|
|
131
|
+
outputTokens = iterOutput;
|
|
132
|
+
} else {
|
|
133
|
+
inputTokens = finite(usage.inputTokens ?? usage.tokens?.input);
|
|
134
|
+
outputTokens = finite(usage.outputTokens ?? usage.tokens?.output);
|
|
135
|
+
cachedTokens = finite(usage.cachedTokens ?? usage.cachedInputTokens ?? usage.tokens?.cache_read);
|
|
136
|
+
reasoningTokens = finite(usage.reasoningTokens ?? usage.tokens?.reasoning);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
if (reasoningTokens === 0 && usage.tokens?.reasoning) {
|
|
140
|
+
reasoningTokens = finite(usage.tokens.reasoning);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
contextUsedTokens = finite(usage.contextWindowUsedTokens);
|
|
144
|
+
contextMaxTokens = finite(usage.contextWindowMaxTokens);
|
|
145
|
+
costUsd = finite(usage.totalCostUsd);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// Count timeline tool calls and tool errors if timeline is provided
|
|
149
|
+
let toolCallsCount = 0;
|
|
150
|
+
let toolErrorsCount = 0;
|
|
151
|
+
if (Array.isArray(raw.timeline)) {
|
|
152
|
+
for (const item of raw.timeline) {
|
|
153
|
+
if (!item || typeof item !== "object") continue;
|
|
154
|
+
const type = (item as any).type;
|
|
155
|
+
if (type === "tool_call" || type === "tool_use" || (item as any).toolCall) {
|
|
156
|
+
toolCallsCount++;
|
|
157
|
+
if ((item as any).isError || (item as any).error || (item as any).status === "error") {
|
|
158
|
+
toolErrorsCount++;
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// Interrupted / canceled normalization
|
|
165
|
+
const kind = raw.outcome?.kind ?? "completed";
|
|
166
|
+
const stopReason = raw.outcome?.stopReason ?? (raw.outcome as any)?.reason;
|
|
167
|
+
const isInterrupted =
|
|
168
|
+
kind === "canceled" ||
|
|
169
|
+
stopReason === "cancelled" ||
|
|
170
|
+
stopReason === "interrupted" ||
|
|
171
|
+
raw.outcome?.reason === "canceled" ||
|
|
172
|
+
raw.outcome?.reason === "interrupted";
|
|
173
|
+
|
|
174
|
+
let finishReason = isInterrupted ? "interrupted" : kind;
|
|
175
|
+
if (kind === "failed") finishReason = "failed";
|
|
176
|
+
|
|
177
|
+
const durationMs = Math.max(0, finite(raw.durationMs));
|
|
178
|
+
const durationSec = durationMs / 1000;
|
|
179
|
+
|
|
180
|
+
// Generation velocity (TPS): sum output + reasoning (OpenCode isolation rule)
|
|
181
|
+
// Interrupted turns with 0 duration or incomplete payloads yield 0 TPS to avoid skewed averages
|
|
182
|
+
const generationTokens = outputTokens + reasoningTokens;
|
|
183
|
+
const tokensPerSecond =
|
|
184
|
+
!isInterrupted && durationSec > 0.05 && generationTokens > 0
|
|
185
|
+
? round2(generationTokens / durationSec)
|
|
186
|
+
: 0;
|
|
187
|
+
|
|
188
|
+
const contextUtilizationPct =
|
|
189
|
+
contextMaxTokens > 0 ? round2(Math.min(100, (contextUsedTokens / contextMaxTokens) * 100)) : 0;
|
|
190
|
+
|
|
191
|
+
// Git churn diff
|
|
192
|
+
let gitInsertions = 0;
|
|
193
|
+
let gitDeletions = 0;
|
|
194
|
+
let gitFilesChanged = 0;
|
|
195
|
+
if (raw.gitAfter) {
|
|
196
|
+
const before = raw.gitBefore;
|
|
197
|
+
gitInsertions = Math.max(0, raw.gitAfter.insertions - (before?.insertions ?? 0));
|
|
198
|
+
gitDeletions = Math.max(0, raw.gitAfter.deletions - (before?.deletions ?? 0));
|
|
199
|
+
gitFilesChanged = raw.gitAfter.filesChanged;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
const isSubscriptionZeroCost = costUsd === 0 && (inputTokens > 0 || outputTokens > 0);
|
|
203
|
+
|
|
204
|
+
return {
|
|
205
|
+
turnId: raw.turnId ?? null,
|
|
206
|
+
agentId: raw.agentId,
|
|
207
|
+
repo: raw.repo ?? null,
|
|
208
|
+
role: raw.role ?? null,
|
|
209
|
+
model: raw.model ?? null,
|
|
210
|
+
provider: raw.provider ?? null,
|
|
211
|
+
timestamp: new Date(now).toISOString(),
|
|
212
|
+
durationMs,
|
|
213
|
+
inputTokens,
|
|
214
|
+
outputTokens,
|
|
215
|
+
cachedTokens,
|
|
216
|
+
reasoningTokens,
|
|
217
|
+
totalTokens: inputTokens + outputTokens + cachedTokens + reasoningTokens,
|
|
218
|
+
tokensPerSecond,
|
|
219
|
+
contextUsedTokens,
|
|
220
|
+
contextMaxTokens,
|
|
221
|
+
contextUtilizationPct,
|
|
222
|
+
costUsd: round2(costUsd),
|
|
223
|
+
toolCallsCount,
|
|
224
|
+
toolErrorsCount,
|
|
225
|
+
finishReason,
|
|
226
|
+
stopReason: stopReason ? String(stopReason) : undefined,
|
|
227
|
+
errorMessage: raw.outcome?.error?.message,
|
|
228
|
+
gitInsertions,
|
|
229
|
+
gitDeletions,
|
|
230
|
+
gitFilesChanged,
|
|
231
|
+
isInterrupted,
|
|
232
|
+
isSubscriptionZeroCost,
|
|
233
|
+
};
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/**
|
|
237
|
+
* 1. Front Desk Suitability Index (FDSI)
|
|
238
|
+
* FDSI = Conversational Conciseness / (TTFT Latency * (Turn Locks + 1) * Token Burn Ratio)
|
|
239
|
+
*/
|
|
240
|
+
export interface FDSIInput {
|
|
241
|
+
conciseness: number;
|
|
242
|
+
ttftLatencyMs: number;
|
|
243
|
+
turnLocks: number;
|
|
244
|
+
tokenBurnRatio: number;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
export function calculateFDSI(input: FDSIInput): number {
|
|
248
|
+
const conciseness = Math.max(0, input.conciseness);
|
|
249
|
+
const ttftSec = Math.max(0.001, input.ttftLatencyMs / 1000);
|
|
250
|
+
const locksFactor = Math.max(1, input.turnLocks + 1);
|
|
251
|
+
const burnRatio = Math.max(1.0, input.tokenBurnRatio);
|
|
252
|
+
|
|
253
|
+
const denominator = ttftSec * locksFactor * burnRatio;
|
|
254
|
+
return round2(conciseness / denominator);
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
/**
|
|
258
|
+
* 2. Worker Implementation ROI (Cost & Churn per Merged PR)
|
|
259
|
+
* Worker ROI = (Net Deliverable Value / (Total Tokens * Wall Clock Time * (Rework Rounds + 1))) / ChurnRatio
|
|
260
|
+
*/
|
|
261
|
+
export interface WorkerROIInput {
|
|
262
|
+
netDeliverableValue: number;
|
|
263
|
+
totalTokens: number;
|
|
264
|
+
wallClockTimeMs: number;
|
|
265
|
+
reworkRounds: number;
|
|
266
|
+
linesAdded: number;
|
|
267
|
+
linesDeleted: number;
|
|
268
|
+
netChanges: number;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
export function calculateWorkerROI(input: WorkerROIInput): number {
|
|
272
|
+
const deliverable = Math.max(0, input.netDeliverableValue);
|
|
273
|
+
const tokens = Math.max(1, input.totalTokens);
|
|
274
|
+
const wallClockSec = Math.max(1, input.wallClockTimeMs / 1000);
|
|
275
|
+
const rework = Math.max(1, input.reworkRounds + 1);
|
|
276
|
+
|
|
277
|
+
const netChanges = Math.max(1, Math.abs(input.netChanges));
|
|
278
|
+
const totalChurn = Math.max(netChanges, input.linesAdded + input.linesDeleted);
|
|
279
|
+
const churnRatio = Math.max(1.0, totalChurn / netChanges);
|
|
280
|
+
|
|
281
|
+
const rawRoi = (deliverable * 1_000_000) / (tokens * wallClockSec * rework);
|
|
282
|
+
return round2(rawRoi / churnRatio);
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
/**
|
|
286
|
+
* 3. Orchestrator Audit Vigilance Score
|
|
287
|
+
* Vigilance = (Flaws Caught in Pre-Flight + False-Positive Claims Debunked) / Total PRs Audited
|
|
288
|
+
*/
|
|
289
|
+
export interface OrchestratorVigilanceInput {
|
|
290
|
+
flawsCaught: number;
|
|
291
|
+
falsePositiveClaimsDebunked: number;
|
|
292
|
+
totalPRsAudited: number;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
export function calculateOrchestratorVigilance(input: OrchestratorVigilanceInput): number {
|
|
296
|
+
const totalAudits = Math.max(1, input.totalPRsAudited);
|
|
297
|
+
const signals = Math.max(0, input.flawsCaught + input.falsePositiveClaimsDebunked);
|
|
298
|
+
return round2(signals / totalAudits);
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
/**
|
|
302
|
+
* 4. Tool Thrashing / Loop Index
|
|
303
|
+
* Frequency of non-zero exit codes (rc != 0) followed immediately by another command
|
|
304
|
+
* without intervening file inspections or edits.
|
|
305
|
+
*/
|
|
306
|
+
export interface ToolCallTrace {
|
|
307
|
+
command?: string;
|
|
308
|
+
exitCode?: number;
|
|
309
|
+
isInspectionOrEdit?: boolean;
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
export function calculateToolThrashingIndex(traces: readonly ToolCallTrace[]): number {
|
|
313
|
+
if (traces.length < 2) return 0;
|
|
314
|
+
|
|
315
|
+
let thrashedSequences = 0;
|
|
316
|
+
let totalCommandTransitions = 0;
|
|
317
|
+
|
|
318
|
+
for (let i = 0; i < traces.length - 1; i++) {
|
|
319
|
+
const curr = traces[i]!;
|
|
320
|
+
const next = traces[i + 1]!;
|
|
321
|
+
|
|
322
|
+
if (curr.exitCode !== undefined) {
|
|
323
|
+
totalCommandTransitions++;
|
|
324
|
+
if (curr.exitCode !== 0 && !curr.isInspectionOrEdit && !next.isInspectionOrEdit) {
|
|
325
|
+
thrashedSequences++;
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
if (totalCommandTransitions === 0) return 0;
|
|
331
|
+
return round2((thrashedSequences / totalCommandTransitions) * 100);
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
/**
|
|
335
|
+
* 5. Context Decay & Amnesia Risk
|
|
336
|
+
* Correlation between context utilization (> 75%) and subsequent turn failures, tool loops, or repetitive questions.
|
|
337
|
+
*/
|
|
338
|
+
export interface ContextTurnTrace {
|
|
339
|
+
contextUtilizationPct: number;
|
|
340
|
+
failed: boolean;
|
|
341
|
+
hasToolLoop?: boolean;
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
export function calculateContextDecayRisk(turns: readonly ContextTurnTrace[]): number {
|
|
345
|
+
if (turns.length === 0) return 0;
|
|
346
|
+
|
|
347
|
+
const highContextTurns = turns.filter((t) => t.contextUtilizationPct > 75);
|
|
348
|
+
if (highContextTurns.length === 0) return 0;
|
|
349
|
+
|
|
350
|
+
const failuresUnderHighContext = highContextTurns.filter(
|
|
351
|
+
(t) => t.failed || t.hasToolLoop,
|
|
352
|
+
).length;
|
|
353
|
+
|
|
354
|
+
return round2((failuresUnderHighContext / highContextTurns.length) * 100);
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
export interface TurnTelemetryTapOptions {
|
|
358
|
+
onRecord?: (record: NormalizedTurnTelemetry) => void | Promise<void>;
|
|
359
|
+
onTurnStarted?: (event: any, context: any) => void;
|
|
360
|
+
onTurnEnded?: (event: any, context: any) => void | Promise<void>;
|
|
361
|
+
context?: any;
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
/**
|
|
365
|
+
* Sets up in-process turn telemetry listeners on the Paseo plugin server (#933).
|
|
366
|
+
*/
|
|
367
|
+
export function setupTurnTelemetryTap(
|
|
368
|
+
server: any,
|
|
369
|
+
options?: TurnTelemetryTapOptions,
|
|
370
|
+
): FeatureDisposer {
|
|
371
|
+
const turnStartTimes = new Map<string, number>();
|
|
372
|
+
const lastKnownUsage = new Map<string, RawTurnUsage>();
|
|
373
|
+
const unsubs: Array<() => void> = [];
|
|
374
|
+
|
|
375
|
+
if (typeof server?.on === "function") {
|
|
376
|
+
// 1. Turn started
|
|
377
|
+
try {
|
|
378
|
+
const unsubStart = server.on("agent.turn_started", (event: any, ctx: any) => {
|
|
379
|
+
if (!event?.agent?.id) return;
|
|
380
|
+
turnStartTimes.set(event.agent.id, Date.now());
|
|
381
|
+
if (event.agent.lastUsage) {
|
|
382
|
+
lastKnownUsage.set(event.agent.id, event.agent.lastUsage);
|
|
383
|
+
}
|
|
384
|
+
options?.onTurnStarted?.(event, ctx);
|
|
385
|
+
});
|
|
386
|
+
if (typeof unsubStart === "function") unsubs.push(unsubStart);
|
|
387
|
+
} catch {}
|
|
388
|
+
|
|
389
|
+
// 2. Usage updated
|
|
390
|
+
try {
|
|
391
|
+
const unsubUsage = server.on("agent.usage_updated", (event: any) => {
|
|
392
|
+
if (!event?.agent?.id) return;
|
|
393
|
+
const usage = event.usage ?? event.agent?.lastUsage;
|
|
394
|
+
if (usage) {
|
|
395
|
+
lastKnownUsage.set(event.agent.id, usage);
|
|
396
|
+
}
|
|
397
|
+
});
|
|
398
|
+
if (typeof unsubUsage === "function") unsubs.push(unsubUsage);
|
|
399
|
+
} catch {}
|
|
400
|
+
|
|
401
|
+
// 3. Turn ended
|
|
402
|
+
try {
|
|
403
|
+
const unsubEnd = server.on("agent.turn_ended", async (event: any, ctx: any) => {
|
|
404
|
+
if (!event?.agent?.id) return;
|
|
405
|
+
const agentId = event.agent.id;
|
|
406
|
+
const startTime = turnStartTimes.get(agentId);
|
|
407
|
+
const durationMs = startTime ? Date.now() - startTime : undefined;
|
|
408
|
+
turnStartTimes.delete(agentId);
|
|
409
|
+
|
|
410
|
+
const cachedUsage = lastKnownUsage.get(agentId);
|
|
411
|
+
const effectiveUsage = (event.agent as any)?.lastUsage ?? cachedUsage ?? null;
|
|
412
|
+
|
|
413
|
+
const rawData: RawTurnData = {
|
|
414
|
+
turnId: event.turnId ?? null,
|
|
415
|
+
agentId,
|
|
416
|
+
repo: (event.agent as any)?.repo ?? null,
|
|
417
|
+
role: (event.agent as any)?.role ?? null,
|
|
418
|
+
model: event.agent.model ?? null,
|
|
419
|
+
provider: event.agent.provider ?? null,
|
|
420
|
+
outcome: event.outcome ?? { kind: "completed" },
|
|
421
|
+
durationMs,
|
|
422
|
+
timeline: event.timeline,
|
|
423
|
+
liveUsage: effectiveUsage,
|
|
424
|
+
};
|
|
425
|
+
|
|
426
|
+
try {
|
|
427
|
+
const record = normalizeTurnTelemetry(rawData);
|
|
428
|
+
if (options?.onRecord) {
|
|
429
|
+
await options.onRecord(record);
|
|
430
|
+
}
|
|
431
|
+
if (options?.onTurnEnded) {
|
|
432
|
+
await options.onTurnEnded(event, ctx);
|
|
433
|
+
}
|
|
434
|
+
} catch {
|
|
435
|
+
// Best effort
|
|
436
|
+
}
|
|
437
|
+
});
|
|
438
|
+
if (typeof unsubEnd === "function") unsubs.push(unsubEnd);
|
|
439
|
+
} catch {}
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
let disposed = false;
|
|
443
|
+
return () => {
|
|
444
|
+
if (disposed) return;
|
|
445
|
+
disposed = true;
|
|
446
|
+
for (const unsub of unsubs) {
|
|
447
|
+
try {
|
|
448
|
+
unsub();
|
|
449
|
+
} catch {}
|
|
450
|
+
}
|
|
451
|
+
turnStartTimes.clear();
|
|
452
|
+
lastKnownUsage.clear();
|
|
453
|
+
};
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
export interface TimelineTelemetryFeatureOptions<TServerContext = any, TClientContext = any>
|
|
457
|
+
extends TurnTelemetryTapOptions {
|
|
458
|
+
id?: string;
|
|
459
|
+
contracts?: Record<string, PluginRpcContract>;
|
|
460
|
+
serverTap?: (server: TServerContext) => FeatureDisposer | void;
|
|
461
|
+
clientRenderer?: (client: TClientContext) => FeatureDisposer | void;
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
/**
|
|
465
|
+
* Creates a reusable FeatureModule for timeline telemetry (#933).
|
|
466
|
+
*/
|
|
467
|
+
export function createTimelineTelemetryFeature<TServerContext = any, TClientContext = any>(
|
|
468
|
+
options: TimelineTelemetryFeatureOptions<TServerContext, TClientContext> = {},
|
|
469
|
+
): FeatureModule<TServerContext, TClientContext> {
|
|
470
|
+
return {
|
|
471
|
+
id: options.id || "timeline-telemetry",
|
|
472
|
+
contracts: options.contracts,
|
|
473
|
+
contributeServer(server: TServerContext) {
|
|
474
|
+
if (options.serverTap) {
|
|
475
|
+
return options.serverTap(server);
|
|
476
|
+
}
|
|
477
|
+
return setupTurnTelemetryTap(server, options);
|
|
478
|
+
},
|
|
479
|
+
contributeClient(client: TClientContext) {
|
|
480
|
+
if (options.clientRenderer) {
|
|
481
|
+
return options.clientRenderer(client);
|
|
482
|
+
}
|
|
483
|
+
},
|
|
484
|
+
};
|
|
485
|
+
}
|
|
486
|
+
|