@senad-d/observme 0.1.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/.env.example +57 -0
- package/CHANGELOG.md +52 -0
- package/LICENSE +21 -0
- package/ObservMe-Production-Docs/00-README.md +79 -0
- package/ObservMe-Production-Docs/01-requirements-and-scope.md +207 -0
- package/ObservMe-Production-Docs/02-reference-architecture.md +306 -0
- package/ObservMe-Production-Docs/03-pi-event-and-session-model.md +266 -0
- package/ObservMe-Production-Docs/04-telemetry-semantic-conventions.md +722 -0
- package/ObservMe-Production-Docs/05-otel-pipeline-and-collector.md +355 -0
- package/ObservMe-Production-Docs/06-security-privacy-redaction.md +294 -0
- package/ObservMe-Production-Docs/07-extension-implementation-blueprint.md +447 -0
- package/ObservMe-Production-Docs/08-query-grafana-integration.md +276 -0
- package/ObservMe-Production-Docs/09-dashboards-alerts-slos.md +640 -0
- package/ObservMe-Production-Docs/10-testing-release-operations.md +319 -0
- package/ObservMe-Production-Docs/11-deployment-runbooks.md +203 -0
- package/ObservMe-Production-Docs/12-configuration-reference.md +337 -0
- package/ObservMe-Production-Docs/13-source-notes.md +33 -0
- package/ObservMe-Production-Docs/pi-session-format.md +427 -0
- package/README.md +356 -0
- package/SECURITY.md +45 -0
- package/dashboards/observme-agent-node-graphs.json +250 -0
- package/dashboards/observme-agents.json +1880 -0
- package/dashboards/observme-alerts.yaml +113 -0
- package/dashboards/observme-branches-compactions.json +1042 -0
- package/dashboards/observme-cost.json +1254 -0
- package/dashboards/observme-errors.json +1659 -0
- package/dashboards/observme-export-health.json +1802 -0
- package/dashboards/observme-latency.json +1494 -0
- package/dashboards/observme-llm-conversations.json +730 -0
- package/dashboards/observme-logs-llm.json +644 -0
- package/dashboards/observme-models.json +933 -0
- package/dashboards/observme-overview.json +2129 -0
- package/dashboards/observme-slo-health.json +737 -0
- package/dashboards/observme-slos.yaml +56 -0
- package/dashboards/observme-tools.json +902 -0
- package/dashboards/observme-trace-journey.json +1793 -0
- package/docs/STRUCTURE.md +49 -0
- package/docs/agent-subagent-observability-requirements.md +997 -0
- package/docs/compatibility-matrix.md +39 -0
- package/docs/configuration-tui-design-standard.md +767 -0
- package/docs/configuration.md +38 -0
- package/docs/review-validation.md +115 -0
- package/docs/validation-flow.md +117 -0
- package/examples/collector.yaml +123 -0
- package/examples/observme.yaml +131 -0
- package/img/demo.gif +0 -0
- package/img/icon.svg +47 -0
- package/package.json +103 -0
- package/src/commands/obs-agents-runtime.ts +150 -0
- package/src/commands/obs-agents.ts +491 -0
- package/src/commands/obs-args.ts +63 -0
- package/src/commands/obs-backfill.ts +1334 -0
- package/src/commands/obs-command-support.ts +43 -0
- package/src/commands/obs-cost.ts +228 -0
- package/src/commands/obs-diagnostics.ts +22 -0
- package/src/commands/obs-errors.ts +156 -0
- package/src/commands/obs-health.ts +301 -0
- package/src/commands/obs-link.ts +90 -0
- package/src/commands/obs-logs.ts +194 -0
- package/src/commands/obs-loki-summary.ts +184 -0
- package/src/commands/obs-session.ts +259 -0
- package/src/commands/obs-status.ts +359 -0
- package/src/commands/obs-tools.ts +274 -0
- package/src/commands/obs-trace.ts +411 -0
- package/src/commands/obs.ts +211 -0
- package/src/config/bootstrap-project-config.ts +300 -0
- package/src/config/defaults.ts +143 -0
- package/src/config/load-config.ts +631 -0
- package/src/config/project-paths.ts +61 -0
- package/src/config/schema.ts +405 -0
- package/src/config/validate.ts +456 -0
- package/src/constants.ts +4 -0
- package/src/diagnostics/sanitize.ts +6 -0
- package/src/extension.ts +38 -0
- package/src/otel/logs.ts +160 -0
- package/src/otel/metrics.ts +165 -0
- package/src/otel/otlp-endpoint.ts +10 -0
- package/src/otel/sdk.ts +114 -0
- package/src/otel/shutdown.ts +102 -0
- package/src/otel/traces.ts +166 -0
- package/src/pi/agent-lineage.ts +378 -0
- package/src/pi/agent-tree-tracker.ts +258 -0
- package/src/pi/event-handlers/agent-turn.ts +155 -0
- package/src/pi/event-handlers/lifecycle.ts +642 -0
- package/src/pi/event-handlers/llm.ts +115 -0
- package/src/pi/event-handlers/session-events.ts +159 -0
- package/src/pi/event-handlers/tool-bash.ts +275 -0
- package/src/pi/handler-internals.ts +2154 -0
- package/src/pi/handler-runtime.ts +633 -0
- package/src/pi/handler-types.ts +261 -0
- package/src/pi/handlers.ts +75 -0
- package/src/pi/subagent-spawn.ts +975 -0
- package/src/pi/subagent-types.ts +29 -0
- package/src/privacy/content-capture.ts +104 -0
- package/src/privacy/hash.ts +93 -0
- package/src/privacy/redact.ts +619 -0
- package/src/privacy/secret-patterns.ts +185 -0
- package/src/privacy/truncate.ts +69 -0
- package/src/query/grafana-readiness.ts +164 -0
- package/src/query/grafana-transport.ts +481 -0
- package/src/query/grafana.ts +371 -0
- package/src/query/loki.ts +332 -0
- package/src/query/prometheus.ts +388 -0
- package/src/query/tempo.ts +332 -0
- package/src/safety/sensitive-input.ts +208 -0
- package/src/semconv/attributes.ts +279 -0
- package/src/semconv/metrics.ts +146 -0
- package/src/semconv/spans.ts +19 -0
- package/src/semconv/values.ts +13 -0
- package/src/util/bounded-map.ts +97 -0
- package/tsconfig.json +15 -0
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
import type { LogResult, TimeRange } from "../query/loki.ts";
|
|
2
|
+
import { LOG_ATTRIBUTES } from "../semconv/attributes.ts";
|
|
3
|
+
|
|
4
|
+
export interface ObsLokiLogSummaryRow {
|
|
5
|
+
readonly timestamp: string;
|
|
6
|
+
readonly eventName: string;
|
|
7
|
+
readonly category?: string;
|
|
8
|
+
readonly severity?: string;
|
|
9
|
+
readonly errorType?: string;
|
|
10
|
+
readonly sessionId?: string;
|
|
11
|
+
readonly traceId?: string;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export interface ObsLokiLogSummaryRenderOptions {
|
|
15
|
+
readonly title: string;
|
|
16
|
+
readonly window: string;
|
|
17
|
+
readonly maxLogs: number;
|
|
18
|
+
readonly rows: readonly ObsLokiLogSummaryRow[];
|
|
19
|
+
readonly emptyMessage: string;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export interface ObsLokiTimeRangeOptions {
|
|
23
|
+
readonly now?: () => Date;
|
|
24
|
+
readonly queryRangeHours?: number;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const defaultRangeHours = 1;
|
|
28
|
+
const minimumMaxLogs = 1;
|
|
29
|
+
const millisecondsPerHour = 60 * 60 * 1000;
|
|
30
|
+
const nanosecondsPerMillisecond = 1_000_000n;
|
|
31
|
+
const maximumDisplayValueLength = 96;
|
|
32
|
+
const safeEventBodyPattern = /^[A-Za-z0-9_.:-]{1,96}$/u;
|
|
33
|
+
const integerNanosecondsPattern = /^-?\d+$/u;
|
|
34
|
+
const unknownTimestamp = "unknown-time";
|
|
35
|
+
const unknownEventName = "structured-log";
|
|
36
|
+
const eventNameAliases = ["event_name", LOG_ATTRIBUTES.EVENT_NAME] as const;
|
|
37
|
+
const eventCategoryAliases = ["event_category", LOG_ATTRIBUTES.EVENT_CATEGORY] as const;
|
|
38
|
+
const severityAliases = [LOG_ATTRIBUTES.SEVERITY, "severity_text", "level"] as const;
|
|
39
|
+
const errorTypeAliases = ["error_type", LOG_ATTRIBUTES.ERROR_TYPE, "error_class"] as const;
|
|
40
|
+
const sessionIdAliases = ["pi_session_id", LOG_ATTRIBUTES.PI_SESSION_ID] as const;
|
|
41
|
+
const traceIdAliases = [LOG_ATTRIBUTES.TRACE_ID, "trace.id"] as const;
|
|
42
|
+
|
|
43
|
+
export function createRecentObsLokiTimeRange(options: ObsLokiTimeRangeOptions = {}): TimeRange {
|
|
44
|
+
const to = normalizeDate(options.now?.() ?? new Date());
|
|
45
|
+
const rangeHours = normalizeRangeHours(options.queryRangeHours);
|
|
46
|
+
return { from: new Date(to.getTime() - rangeHours * millisecondsPerHour), to };
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function formatObsLokiWindow(options: ObsLokiTimeRangeOptions = {}): string {
|
|
50
|
+
return `${trimTrailingFractionZeros(String(normalizeRangeHours(options.queryRangeHours)))}h`;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export function normalizeObsLokiMaxLogs(value: number | undefined): number {
|
|
54
|
+
if (value === undefined || !Number.isFinite(value) || value < minimumMaxLogs) return minimumMaxLogs;
|
|
55
|
+
return Math.trunc(value);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export function toObsLokiLogSummaryRow(log: LogResult): ObsLokiLogSummaryRow {
|
|
59
|
+
return {
|
|
60
|
+
timestamp: formatLogTimestamp(log.timestampUnixNano),
|
|
61
|
+
eventName: resolveLogEventName(log),
|
|
62
|
+
category: readFirstLogField(log, eventCategoryAliases),
|
|
63
|
+
severity: readFirstLogField(log, severityAliases),
|
|
64
|
+
errorType: readFirstLogField(log, errorTypeAliases),
|
|
65
|
+
sessionId: readFirstLogField(log, sessionIdAliases),
|
|
66
|
+
traceId: readFirstLogField(log, traceIdAliases),
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export function renderObsLokiLogSummary(options: ObsLokiLogSummaryRenderOptions): string {
|
|
71
|
+
const maxLogs = normalizeObsLokiMaxLogs(options.maxLogs);
|
|
72
|
+
const rows = options.rows.map(normalizeObsLokiLogSummaryRow).filter(isObsLokiLogSummaryRow).slice(0, maxLogs);
|
|
73
|
+
const lines = [`${options.title} (last ${options.window}, max ${maxLogs})`];
|
|
74
|
+
|
|
75
|
+
if (rows.length === 0) {
|
|
76
|
+
lines.push(options.emptyMessage);
|
|
77
|
+
return lines.join("\n");
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
lines.push(...rows.map(renderObsLokiLogSummaryRow));
|
|
81
|
+
return lines.join("\n");
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function normalizeDate(value: Date): Date {
|
|
85
|
+
if (value instanceof Date && Number.isFinite(value.getTime())) return value;
|
|
86
|
+
return new Date();
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function normalizeRangeHours(value: number | undefined): number {
|
|
90
|
+
if (value === undefined || !Number.isFinite(value) || value <= 0) return defaultRangeHours;
|
|
91
|
+
return value;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function formatLogTimestamp(timestampUnixNano: string): string {
|
|
95
|
+
const timestamp = timestampUnixNano.trim();
|
|
96
|
+
if (!timestamp) return unknownTimestamp;
|
|
97
|
+
|
|
98
|
+
if (!integerNanosecondsPattern.test(timestamp)) return timestamp;
|
|
99
|
+
|
|
100
|
+
const milliseconds = BigInt(timestamp) / nanosecondsPerMillisecond;
|
|
101
|
+
if (milliseconds < 0n || milliseconds > BigInt(Number.MAX_SAFE_INTEGER)) return timestamp;
|
|
102
|
+
return new Date(Number(milliseconds)).toISOString();
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function resolveLogEventName(log: LogResult): string {
|
|
106
|
+
return readFirstLogField(log, eventNameAliases) ?? readSafeLogLineEventName(log.line) ?? unknownEventName;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function readSafeLogLineEventName(line: string): string | undefined {
|
|
110
|
+
const value = normalizeDisplayValue(line);
|
|
111
|
+
if (!value || !safeEventBodyPattern.test(value)) return undefined;
|
|
112
|
+
return value;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function readFirstLogField(log: LogResult, aliases: readonly string[]): string | undefined {
|
|
116
|
+
for (const alias of aliases) {
|
|
117
|
+
const metadataValue = normalizeDisplayValue(log.metadata?.[alias]);
|
|
118
|
+
if (metadataValue) return metadataValue;
|
|
119
|
+
|
|
120
|
+
const labelValue = normalizeDisplayValue(log.labels[alias]);
|
|
121
|
+
if (labelValue) return labelValue;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
return undefined;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function normalizeObsLokiLogSummaryRow(row: ObsLokiLogSummaryRow): ObsLokiLogSummaryRow | undefined {
|
|
128
|
+
const timestamp = normalizeDisplayValue(row.timestamp) ?? unknownTimestamp;
|
|
129
|
+
const eventName = normalizeDisplayValue(row.eventName) ?? unknownEventName;
|
|
130
|
+
|
|
131
|
+
return {
|
|
132
|
+
timestamp,
|
|
133
|
+
eventName,
|
|
134
|
+
category: normalizeDisplayValue(row.category),
|
|
135
|
+
severity: normalizeDisplayValue(row.severity),
|
|
136
|
+
errorType: normalizeDisplayValue(row.errorType),
|
|
137
|
+
sessionId: normalizeDisplayValue(row.sessionId),
|
|
138
|
+
traceId: normalizeDisplayValue(row.traceId),
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function normalizeDisplayValue(value: string | undefined): string | undefined {
|
|
143
|
+
const normalized = value?.replace(/\s+/gu, " ").trim();
|
|
144
|
+
if (!normalized) return undefined;
|
|
145
|
+
if (normalized.length <= maximumDisplayValueLength) return normalized;
|
|
146
|
+
return `${normalized.slice(0, maximumDisplayValueLength - 1)}…`;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function renderObsLokiLogSummaryRow(row: ObsLokiLogSummaryRow): string {
|
|
150
|
+
return [
|
|
151
|
+
row.timestamp,
|
|
152
|
+
row.eventName,
|
|
153
|
+
renderOptionalField("category", row.category),
|
|
154
|
+
renderOptionalField(LOG_ATTRIBUTES.SEVERITY, row.severity),
|
|
155
|
+
renderOptionalField("error", row.errorType),
|
|
156
|
+
renderOptionalField("session", row.sessionId),
|
|
157
|
+
renderOptionalField("trace", row.traceId),
|
|
158
|
+
]
|
|
159
|
+
.filter(isString)
|
|
160
|
+
.join(" ");
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function renderOptionalField(label: string, value: string | undefined): string | undefined {
|
|
164
|
+
if (!value) return undefined;
|
|
165
|
+
return `${label}=${value}`;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
function isObsLokiLogSummaryRow(row: ObsLokiLogSummaryRow | undefined): row is ObsLokiLogSummaryRow {
|
|
169
|
+
return row !== undefined;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function isString(value: string | undefined): value is string {
|
|
173
|
+
return value !== undefined && value.length > 0;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
function trimTrailingFractionZeros(value: string): string {
|
|
177
|
+
if (!value.includes(".")) return value;
|
|
178
|
+
|
|
179
|
+
let end = value.length;
|
|
180
|
+
while (end > 0 && value[end - 1] === "0") end -= 1;
|
|
181
|
+
|
|
182
|
+
const withoutTrailingZeros = value.slice(0, end);
|
|
183
|
+
return withoutTrailingZeros.endsWith(".") ? withoutTrailingZeros.slice(0, -1) : withoutTrailingZeros;
|
|
184
|
+
}
|
|
@@ -0,0 +1,259 @@
|
|
|
1
|
+
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import { completeObsSubcommand, isExactObsSubcommandRequest } from "./obs-args.ts";
|
|
3
|
+
|
|
4
|
+
export interface ObsSessionCommandContext {
|
|
5
|
+
readonly ui: {
|
|
6
|
+
notify: (message: string, type?: "info" | "warning" | "error") => Promise<void> | void;
|
|
7
|
+
};
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export interface ObsSessionSnapshot {
|
|
11
|
+
readonly sessionId?: string;
|
|
12
|
+
readonly traceId?: string;
|
|
13
|
+
readonly turns: number;
|
|
14
|
+
readonly llmCalls: number;
|
|
15
|
+
readonly toolCalls: number;
|
|
16
|
+
readonly costUsd: number;
|
|
17
|
+
readonly traceLink?: string;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export interface ObsSessionRuntimeStatePatch {
|
|
21
|
+
readonly sessionId?: string;
|
|
22
|
+
readonly traceId?: string;
|
|
23
|
+
readonly turns?: number;
|
|
24
|
+
readonly llmCalls?: number;
|
|
25
|
+
readonly toolCalls?: number;
|
|
26
|
+
readonly costUsd?: number;
|
|
27
|
+
readonly traceLink?: string;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export interface StartObsSessionRuntimeStateOptions {
|
|
31
|
+
readonly sessionId?: string;
|
|
32
|
+
readonly traceId?: string;
|
|
33
|
+
readonly traceUrlTemplate?: string;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export type ObsSessionProvider = (ctx: ObsSessionCommandContext) => Promise<ObsSessionSnapshot> | ObsSessionSnapshot;
|
|
37
|
+
|
|
38
|
+
export interface RegisterObsSessionCommandOptions {
|
|
39
|
+
readonly getSession?: ObsSessionProvider;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
interface MutableObsSessionRuntimeState {
|
|
43
|
+
sessionId?: string;
|
|
44
|
+
traceId?: string;
|
|
45
|
+
turns: number;
|
|
46
|
+
llmCalls: number;
|
|
47
|
+
toolCalls: number;
|
|
48
|
+
costUsd: number;
|
|
49
|
+
traceLink?: string;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const OBS_COMMAND_NAME = "obs";
|
|
53
|
+
const OBS_SESSION_SUBCOMMAND = "session";
|
|
54
|
+
const traceIdPattern = /^[a-f0-9]{32}$/iu;
|
|
55
|
+
|
|
56
|
+
const runtimeSessionState: MutableObsSessionRuntimeState = createEmptyObsSessionRuntimeState();
|
|
57
|
+
|
|
58
|
+
export function registerObsSessionCommand(pi: ExtensionAPI, options: RegisterObsSessionCommandOptions = {}): void {
|
|
59
|
+
const command = new ObsSessionCommand(options);
|
|
60
|
+
|
|
61
|
+
pi.registerCommand(OBS_COMMAND_NAME, {
|
|
62
|
+
description: "Show current ObservMe session telemetry. Usage: /obs session",
|
|
63
|
+
getArgumentCompletions: getObsSessionCommandArgumentCompletions,
|
|
64
|
+
handler: command.handle.bind(command),
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export async function handleObsSessionCommand(
|
|
69
|
+
args: string,
|
|
70
|
+
ctx: ObsSessionCommandContext,
|
|
71
|
+
options: RegisterObsSessionCommandOptions = {},
|
|
72
|
+
): Promise<void> {
|
|
73
|
+
if (!isObsSessionRequest(args)) {
|
|
74
|
+
await notifySession(ctx, "Usage: /obs session", "warning");
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
try {
|
|
79
|
+
const snapshot = await resolveObsSessionSnapshot(ctx, options);
|
|
80
|
+
await notifySession(ctx, renderObsSession(snapshot), "info");
|
|
81
|
+
} catch (error) {
|
|
82
|
+
await notifySession(ctx, `ObservMe session unavailable: ${formatError(error)}`, "error");
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export function getObsSessionCommandArgumentCompletions(prefix: string): Array<{ value: string; label: string }> | null {
|
|
87
|
+
return completeObsSubcommand(prefix, OBS_SESSION_SUBCOMMAND);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export function getLocalObsSessionSnapshot(): ObsSessionSnapshot {
|
|
91
|
+
return normalizeObsSessionSnapshot(runtimeSessionState);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export function renderObsSession(snapshot: ObsSessionSnapshot): string {
|
|
95
|
+
const normalized = normalizeObsSessionSnapshot(snapshot);
|
|
96
|
+
const lines = [
|
|
97
|
+
`Session: ${normalized.sessionId ?? "unknown"}`,
|
|
98
|
+
`Trace: ${normalized.traceId ?? "unavailable"}`,
|
|
99
|
+
`Turns: ${normalized.turns}`,
|
|
100
|
+
`LLM calls: ${normalized.llmCalls}`,
|
|
101
|
+
`Tool calls: ${normalized.toolCalls}`,
|
|
102
|
+
`Cost: ${formatUsd(normalized.costUsd)}`,
|
|
103
|
+
];
|
|
104
|
+
|
|
105
|
+
if (normalized.traceLink) lines.push(`Open trace: ${normalized.traceLink}`);
|
|
106
|
+
return lines.join("\n");
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
export function startObsSessionRuntimeState(options: StartObsSessionRuntimeStateOptions): void {
|
|
110
|
+
const traceId = normalizeTraceId(options.traceId);
|
|
111
|
+
const traceLink = buildObsSessionTraceLink(traceId, options.traceUrlTemplate);
|
|
112
|
+
|
|
113
|
+
replaceObsSessionRuntimeState({
|
|
114
|
+
sessionId: normalizeString(options.sessionId),
|
|
115
|
+
traceId,
|
|
116
|
+
turns: 0,
|
|
117
|
+
llmCalls: 0,
|
|
118
|
+
toolCalls: 0,
|
|
119
|
+
costUsd: 0,
|
|
120
|
+
traceLink,
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
export function updateObsSessionRuntimeState(patch: ObsSessionRuntimeStatePatch): void {
|
|
125
|
+
if (patch.sessionId !== undefined) runtimeSessionState.sessionId = normalizeString(patch.sessionId);
|
|
126
|
+
if (patch.traceId !== undefined) runtimeSessionState.traceId = normalizeTraceId(patch.traceId);
|
|
127
|
+
if (patch.turns !== undefined) runtimeSessionState.turns = normalizeCount(patch.turns);
|
|
128
|
+
if (patch.llmCalls !== undefined) runtimeSessionState.llmCalls = normalizeCount(patch.llmCalls);
|
|
129
|
+
if (patch.toolCalls !== undefined) runtimeSessionState.toolCalls = normalizeCount(patch.toolCalls);
|
|
130
|
+
if (patch.costUsd !== undefined) runtimeSessionState.costUsd = normalizeCost(patch.costUsd);
|
|
131
|
+
if (patch.traceLink !== undefined) runtimeSessionState.traceLink = normalizeString(patch.traceLink);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
export function recordObsSessionTurn(count = 1): void {
|
|
135
|
+
runtimeSessionState.turns += normalizeCount(count);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
export function recordObsSessionLlmCall(count = 1): void {
|
|
139
|
+
runtimeSessionState.llmCalls += normalizeCount(count);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
export function recordObsSessionToolCall(count = 1): void {
|
|
143
|
+
runtimeSessionState.toolCalls += normalizeCount(count);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
export function recordObsSessionCost(costUsd: number | undefined): void {
|
|
147
|
+
if (costUsd === undefined) return;
|
|
148
|
+
runtimeSessionState.costUsd = normalizeCost(runtimeSessionState.costUsd + normalizeCost(costUsd));
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
export function clearObsSessionRuntimeState(): void {
|
|
152
|
+
replaceObsSessionRuntimeState(createEmptyObsSessionRuntimeState());
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
export function buildObsSessionTraceLink(traceId: string | undefined, traceUrlTemplate: string | undefined): string | undefined {
|
|
156
|
+
const template = normalizeString(traceUrlTemplate);
|
|
157
|
+
if (!traceId || !template) return undefined;
|
|
158
|
+
|
|
159
|
+
if (template.includes("{{traceId}}")) return template.replaceAll("{{traceId}}", encodeURIComponent(traceId));
|
|
160
|
+
if (template.includes("${traceId}")) return template.replaceAll("${traceId}", encodeURIComponent(traceId));
|
|
161
|
+
if (template.includes("{traceId}")) return template.replaceAll("{traceId}", encodeURIComponent(traceId));
|
|
162
|
+
if (template.includes("$traceId")) return template.replaceAll("$traceId", encodeURIComponent(traceId));
|
|
163
|
+
if (template.includes("%TRACE_ID%")) return template.replaceAll("%TRACE_ID%", encodeURIComponent(traceId));
|
|
164
|
+
if (template.includes("__TRACE_ID__")) return template.replaceAll("__TRACE_ID__", encodeURIComponent(traceId));
|
|
165
|
+
if (template.includes("...")) return template.replaceAll("...", encodeURIComponent(traceId));
|
|
166
|
+
return undefined;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
class ObsSessionCommand {
|
|
170
|
+
readonly #options: RegisterObsSessionCommandOptions;
|
|
171
|
+
|
|
172
|
+
constructor(options: RegisterObsSessionCommandOptions) {
|
|
173
|
+
this.#options = options;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
async handle(args: string, ctx: ObsSessionCommandContext): Promise<void> {
|
|
177
|
+
await handleObsSessionCommand(args, ctx, this.#options);
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
async function resolveObsSessionSnapshot(
|
|
182
|
+
ctx: ObsSessionCommandContext,
|
|
183
|
+
options: RegisterObsSessionCommandOptions,
|
|
184
|
+
): Promise<ObsSessionSnapshot> {
|
|
185
|
+
if (options.getSession) return options.getSession(ctx);
|
|
186
|
+
return getLocalObsSessionSnapshot();
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
function replaceObsSessionRuntimeState(state: MutableObsSessionRuntimeState): void {
|
|
190
|
+
runtimeSessionState.sessionId = state.sessionId;
|
|
191
|
+
runtimeSessionState.traceId = state.traceId;
|
|
192
|
+
runtimeSessionState.turns = state.turns;
|
|
193
|
+
runtimeSessionState.llmCalls = state.llmCalls;
|
|
194
|
+
runtimeSessionState.toolCalls = state.toolCalls;
|
|
195
|
+
runtimeSessionState.costUsd = state.costUsd;
|
|
196
|
+
runtimeSessionState.traceLink = state.traceLink;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
function createEmptyObsSessionRuntimeState(): MutableObsSessionRuntimeState {
|
|
200
|
+
return {
|
|
201
|
+
turns: 0,
|
|
202
|
+
llmCalls: 0,
|
|
203
|
+
toolCalls: 0,
|
|
204
|
+
costUsd: 0,
|
|
205
|
+
};
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
function normalizeObsSessionSnapshot(snapshot: ObsSessionSnapshot): ObsSessionSnapshot {
|
|
209
|
+
return {
|
|
210
|
+
sessionId: normalizeString(snapshot.sessionId),
|
|
211
|
+
traceId: normalizeTraceId(snapshot.traceId),
|
|
212
|
+
turns: normalizeCount(snapshot.turns),
|
|
213
|
+
llmCalls: normalizeCount(snapshot.llmCalls),
|
|
214
|
+
toolCalls: normalizeCount(snapshot.toolCalls),
|
|
215
|
+
costUsd: normalizeCost(snapshot.costUsd),
|
|
216
|
+
traceLink: normalizeString(snapshot.traceLink),
|
|
217
|
+
};
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
function normalizeString(value: string | undefined): string | undefined {
|
|
221
|
+
const trimmed = value?.trim();
|
|
222
|
+
return trimmed || undefined;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
function normalizeTraceId(value: string | undefined): string | undefined {
|
|
226
|
+
const trimmed = normalizeString(value);
|
|
227
|
+
if (!trimmed || !traceIdPattern.test(trimmed) || /^0+$/u.test(trimmed)) return undefined;
|
|
228
|
+
return trimmed.toLowerCase();
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
function normalizeCount(value: number | undefined): number {
|
|
232
|
+
if (value === undefined || !Number.isFinite(value) || value < 0) return 0;
|
|
233
|
+
return Math.trunc(value);
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
function normalizeCost(value: number | undefined): number {
|
|
237
|
+
if (value === undefined || !Number.isFinite(value) || value < 0) return 0;
|
|
238
|
+
return value;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
function formatUsd(value: number): string {
|
|
242
|
+
return `$${normalizeCost(value).toFixed(2)}`;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
function isObsSessionRequest(args: string): boolean {
|
|
246
|
+
return isExactObsSubcommandRequest(args, OBS_SESSION_SUBCOMMAND);
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
async function notifySession(
|
|
250
|
+
ctx: ObsSessionCommandContext,
|
|
251
|
+
message: string,
|
|
252
|
+
type: "info" | "warning" | "error",
|
|
253
|
+
): Promise<void> {
|
|
254
|
+
await ctx.ui?.notify?.(message, type);
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
function formatError(error: unknown): string {
|
|
258
|
+
return error instanceof Error ? error.message : String(error);
|
|
259
|
+
}
|