@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,411 @@
|
|
|
1
|
+
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import type { LoadSessionConfigOptions } from "../config/load-config.ts";
|
|
3
|
+
import type { ObservMeConfig } from "../config/schema.ts";
|
|
4
|
+
import { createGrafanaQueryClient, type GrafanaFetch } from "../query/grafana.ts";
|
|
5
|
+
import { normalizeObservMeSessionId } from "../safety/sensitive-input.ts";
|
|
6
|
+
import type { TimeRange, TraceSummary } from "../query/tempo.ts";
|
|
7
|
+
import {
|
|
8
|
+
completeObsSubcommand,
|
|
9
|
+
missingObsOptionValueMessage,
|
|
10
|
+
obsUsageWithError,
|
|
11
|
+
parseObsSubcommandArgs,
|
|
12
|
+
unknownObsOptionMessage,
|
|
13
|
+
} from "./obs-args.ts";
|
|
14
|
+
import { loadObsCommandConfig, notifyObsCommand } from "./obs-command-support.ts";
|
|
15
|
+
import { formatObsCommandFailure, readObsDiagnosticMessage, type ObsCommandRecoveryHint } from "./obs-diagnostics.ts";
|
|
16
|
+
import { searchTempo } from "../query/tempo.ts";
|
|
17
|
+
import { COMMON_SPAN_ATTRIBUTES } from "../semconv/attributes.ts";
|
|
18
|
+
import type { ObsSessionSnapshot } from "./obs-session.ts";
|
|
19
|
+
import { getLocalObsSessionSnapshot } from "./obs-session.ts";
|
|
20
|
+
|
|
21
|
+
export interface ObsTraceCommandContext {
|
|
22
|
+
readonly cwd?: string;
|
|
23
|
+
readonly ui: {
|
|
24
|
+
notify: (message: string, type?: "info" | "warning" | "error") => Promise<void> | void;
|
|
25
|
+
};
|
|
26
|
+
readonly isProjectTrusted?: () => boolean | Promise<boolean>;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export type ObsTraceScope = "current-session" | "last-turn" | "session";
|
|
30
|
+
export type ObsTraceSource = "runtime" | "tempo";
|
|
31
|
+
export type ObsTraceConfigLoader = (options: LoadSessionConfigOptions) => Promise<ObservMeConfig>;
|
|
32
|
+
export type ObsTraceSessionSnapshot = Pick<ObsSessionSnapshot, "sessionId" | "traceId" | "turns">;
|
|
33
|
+
export type ObsTraceSessionProvider = (
|
|
34
|
+
ctx: ObsTraceCommandContext,
|
|
35
|
+
) => Promise<ObsTraceSessionSnapshot> | ObsTraceSessionSnapshot;
|
|
36
|
+
export type ObsTraceSessionTraceResolver = (
|
|
37
|
+
sessionId: string,
|
|
38
|
+
ctx: ObsTraceCommandContext,
|
|
39
|
+
config: ObservMeConfig,
|
|
40
|
+
) => Promise<string | undefined> | string | undefined;
|
|
41
|
+
export type ObsTraceProvider = (
|
|
42
|
+
ctx: ObsTraceCommandContext,
|
|
43
|
+
request: ObsTraceRequest,
|
|
44
|
+
) => Promise<ObsTraceSnapshot> | ObsTraceSnapshot;
|
|
45
|
+
|
|
46
|
+
export interface ObsTraceRequest {
|
|
47
|
+
readonly scope: ObsTraceScope;
|
|
48
|
+
readonly sessionId?: string;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export interface ObsTraceSnapshot {
|
|
52
|
+
readonly scope: ObsTraceScope;
|
|
53
|
+
readonly source: ObsTraceSource;
|
|
54
|
+
readonly traceId: string;
|
|
55
|
+
readonly traceLink: string;
|
|
56
|
+
readonly sessionId?: string;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export interface ObsTraceSnapshotOptions {
|
|
60
|
+
readonly loadConfig?: ObsTraceConfigLoader;
|
|
61
|
+
readonly fetch?: GrafanaFetch;
|
|
62
|
+
readonly env?: NodeJS.ProcessEnv;
|
|
63
|
+
readonly configDirName?: string;
|
|
64
|
+
readonly getSession?: ObsTraceSessionProvider;
|
|
65
|
+
readonly resolveSessionTraceId?: ObsTraceSessionTraceResolver;
|
|
66
|
+
readonly searchRangeHours?: number;
|
|
67
|
+
readonly now?: () => Date;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export interface RegisterObsTraceCommandOptions extends ObsTraceSnapshotOptions {
|
|
71
|
+
readonly getTrace?: ObsTraceProvider;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
interface ObsTraceTarget {
|
|
75
|
+
readonly scope: ObsTraceScope;
|
|
76
|
+
readonly source: ObsTraceSource;
|
|
77
|
+
readonly traceId: string;
|
|
78
|
+
readonly sessionId?: string;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export interface ParsedObsTraceRequest {
|
|
82
|
+
readonly request?: ObsTraceRequest;
|
|
83
|
+
readonly error?: string;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
interface ObsTraceParseState {
|
|
87
|
+
request?: ObsTraceRequest;
|
|
88
|
+
index: number;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const OBS_COMMAND_NAME = "obs";
|
|
92
|
+
const OBS_TRACE_SUBCOMMAND = "trace";
|
|
93
|
+
const OBS_TRACE_USAGE = "Usage: /obs trace [--last-turn|--session <session-id>]";
|
|
94
|
+
const OBS_TRACE_TEMPO_ERROR_NEXT_ACTION = "run /obs health and verify query.grafana.url, Grafana credentials, and the Tempo datasource UID.";
|
|
95
|
+
const OBS_TRACE_SESSION_ERROR_NEXT_ACTION = "wait for a Pi turn or query a generated session id with /obs trace --session <session-id>.";
|
|
96
|
+
const OBS_TRACE_NOT_FOUND_NEXT_ACTION = "check the session id, wait for trace export, then verify Tempo datasource with /obs health.";
|
|
97
|
+
const OBS_TRACE_ACTIVE_SESSION_NOTE = "Trace visibility: active sessions may show ended child spans before the root pi.session span; the root is exported after session_shutdown.";
|
|
98
|
+
const DEFAULT_TRACE_SEARCH_RANGE_HOURS = 24;
|
|
99
|
+
const millisecondsPerHour = 60 * 60 * 1000;
|
|
100
|
+
const defaultObsTraceRequest = { scope: "current-session" } as const satisfies ObsTraceRequest;
|
|
101
|
+
|
|
102
|
+
export function registerObsTraceCommand(pi: ExtensionAPI, options: RegisterObsTraceCommandOptions = {}): void {
|
|
103
|
+
const command = new ObsTraceCommand(options);
|
|
104
|
+
|
|
105
|
+
pi.registerCommand(OBS_COMMAND_NAME, {
|
|
106
|
+
description: "Show an ObservMe Grafana trace link. Usage: /obs trace [--last-turn|--session <session-id>]",
|
|
107
|
+
getArgumentCompletions: getObsTraceCommandArgumentCompletions,
|
|
108
|
+
handler: command.handle.bind(command),
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
export async function handleObsTraceCommand(
|
|
113
|
+
args: string,
|
|
114
|
+
ctx: ObsTraceCommandContext,
|
|
115
|
+
options: RegisterObsTraceCommandOptions = {},
|
|
116
|
+
): Promise<void> {
|
|
117
|
+
const parsed = parseObsTraceArgsForSubcommand(args, OBS_TRACE_SUBCOMMAND);
|
|
118
|
+
|
|
119
|
+
if (!parsed.request) {
|
|
120
|
+
await notifyObsCommand(ctx, obsUsageWithError(OBS_TRACE_USAGE, parsed.error), "warning");
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
const request = parsed.request;
|
|
125
|
+
|
|
126
|
+
try {
|
|
127
|
+
const snapshot = await resolveObsTraceSnapshot(ctx, request, options);
|
|
128
|
+
await notifyObsCommand(ctx, renderObsTrace(snapshot), "info");
|
|
129
|
+
} catch (error) {
|
|
130
|
+
await notifyObsCommand(ctx, formatObsCommandFailure("ObservMe trace unavailable", error, resolveObsTraceDiagnostic(error)), "error");
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
export function getObsTraceCommandArgumentCompletions(prefix: string): Array<{ value: string; label: string }> | null {
|
|
135
|
+
return completeObsSubcommand(prefix, OBS_TRACE_SUBCOMMAND);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
export async function getObsTraceSnapshot(
|
|
139
|
+
ctx: ObsTraceCommandContext,
|
|
140
|
+
request: ObsTraceRequest = defaultObsTraceRequest,
|
|
141
|
+
options: ObsTraceSnapshotOptions = {},
|
|
142
|
+
): Promise<ObsTraceSnapshot> {
|
|
143
|
+
const config = await loadObsTraceConfig(ctx, options);
|
|
144
|
+
const target = await resolveObsTraceTarget(ctx, request, config, options);
|
|
145
|
+
const client = createGrafanaQueryClient(config, { fetch: options.fetch });
|
|
146
|
+
const traceLink = client.getTraceLink(target.traceId);
|
|
147
|
+
|
|
148
|
+
return { ...target, traceLink };
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
export async function resolveObsTraceSnapshot(
|
|
152
|
+
ctx: ObsTraceCommandContext,
|
|
153
|
+
request: ObsTraceRequest,
|
|
154
|
+
options: RegisterObsTraceCommandOptions,
|
|
155
|
+
): Promise<ObsTraceSnapshot> {
|
|
156
|
+
if (options.getTrace) return options.getTrace(ctx, request);
|
|
157
|
+
return getObsTraceSnapshot(ctx, request, options);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
export function renderObsTrace(snapshot: ObsTraceSnapshot): string {
|
|
161
|
+
return renderObsTraceWithTitle(snapshot, "Trace link");
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
export function renderObsTraceWithTitle(snapshot: ObsTraceSnapshot, title: string): string {
|
|
165
|
+
const lines = [`${title} (${formatObsTraceScope(snapshot.scope)})`];
|
|
166
|
+
const sessionId = normalizeOptionalString(snapshot.sessionId);
|
|
167
|
+
|
|
168
|
+
if (sessionId) lines.push(`Session: ${sessionId}`);
|
|
169
|
+
lines.push(`Trace: ${snapshot.traceId}`, `Open trace: ${snapshot.traceLink}`);
|
|
170
|
+
const visibilityNote = formatObsTraceVisibilityNote(snapshot);
|
|
171
|
+
if (visibilityNote) lines.push(visibilityNote);
|
|
172
|
+
return lines.join("\n");
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
export function parseObsTraceRequestForSubcommand(args: string, subcommand: string): ObsTraceRequest | undefined {
|
|
176
|
+
return parseObsTraceArgsForSubcommand(args, subcommand).request;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
export function parseObsTraceArgsForSubcommand(args: string, subcommand: string): ParsedObsTraceRequest {
|
|
180
|
+
const parsed = parseObsSubcommandArgs(args, subcommand);
|
|
181
|
+
if (!parsed.matched) return {};
|
|
182
|
+
return parseObsTraceOptions(parsed.values);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
class ObsTraceCommand {
|
|
186
|
+
readonly #options: RegisterObsTraceCommandOptions;
|
|
187
|
+
|
|
188
|
+
constructor(options: RegisterObsTraceCommandOptions) {
|
|
189
|
+
this.#options = options;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
async handle(args: string, ctx: ObsTraceCommandContext): Promise<void> {
|
|
193
|
+
await handleObsTraceCommand(args, ctx, this.#options);
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
async function loadObsTraceConfig(
|
|
198
|
+
ctx: ObsTraceCommandContext,
|
|
199
|
+
options: ObsTraceSnapshotOptions,
|
|
200
|
+
): Promise<ObservMeConfig> {
|
|
201
|
+
return loadObsCommandConfig(ctx, options);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
async function resolveObsTraceTarget(
|
|
205
|
+
ctx: ObsTraceCommandContext,
|
|
206
|
+
request: ObsTraceRequest,
|
|
207
|
+
config: ObservMeConfig,
|
|
208
|
+
options: ObsTraceSnapshotOptions,
|
|
209
|
+
): Promise<ObsTraceTarget> {
|
|
210
|
+
const session = await resolveObsTraceSession(ctx, options);
|
|
211
|
+
|
|
212
|
+
if (request.scope === "current-session") return resolveCurrentSessionTraceTarget(session);
|
|
213
|
+
if (request.scope === "last-turn") return resolveLastTurnTraceTarget(session);
|
|
214
|
+
return resolveRequestedSessionTraceTarget(ctx, request, session, config, options);
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
async function resolveObsTraceSession(
|
|
218
|
+
ctx: ObsTraceCommandContext,
|
|
219
|
+
options: ObsTraceSnapshotOptions,
|
|
220
|
+
): Promise<ObsTraceSessionSnapshot> {
|
|
221
|
+
if (options.getSession) return options.getSession(ctx);
|
|
222
|
+
return getLocalObsSessionSnapshot();
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
function resolveCurrentSessionTraceTarget(session: ObsTraceSessionSnapshot): ObsTraceTarget {
|
|
226
|
+
const traceId = normalizeOptionalString(session.traceId);
|
|
227
|
+
if (!traceId) throw new Error("No current ObservMe session trace is available.");
|
|
228
|
+
|
|
229
|
+
return {
|
|
230
|
+
scope: "current-session",
|
|
231
|
+
source: "runtime",
|
|
232
|
+
sessionId: normalizeOptionalString(session.sessionId),
|
|
233
|
+
traceId,
|
|
234
|
+
};
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
function resolveLastTurnTraceTarget(session: ObsTraceSessionSnapshot): ObsTraceTarget {
|
|
238
|
+
const traceId = normalizeOptionalString(session.traceId);
|
|
239
|
+
|
|
240
|
+
if (normalizeTurnCount(session.turns) < 1) throw new Error("No last-turn ObservMe trace is available yet.");
|
|
241
|
+
if (!traceId) throw new Error("No last-turn ObservMe trace is available yet.");
|
|
242
|
+
|
|
243
|
+
return {
|
|
244
|
+
scope: "last-turn",
|
|
245
|
+
source: "runtime",
|
|
246
|
+
sessionId: normalizeOptionalString(session.sessionId),
|
|
247
|
+
traceId,
|
|
248
|
+
};
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
async function resolveRequestedSessionTraceTarget(
|
|
252
|
+
ctx: ObsTraceCommandContext,
|
|
253
|
+
request: ObsTraceRequest,
|
|
254
|
+
session: ObsTraceSessionSnapshot,
|
|
255
|
+
config: ObservMeConfig,
|
|
256
|
+
options: ObsTraceSnapshotOptions,
|
|
257
|
+
): Promise<ObsTraceTarget> {
|
|
258
|
+
const sessionId = normalizeObsTraceSessionId(request.sessionId);
|
|
259
|
+
const localTraceId = resolveLocalTraceIdForSession(sessionId, session);
|
|
260
|
+
|
|
261
|
+
if (localTraceId) return { scope: "session", source: "runtime", sessionId, traceId: localTraceId };
|
|
262
|
+
|
|
263
|
+
const traceId = await resolveSessionTraceId(sessionId, ctx, config, options);
|
|
264
|
+
if (!traceId) throw new Error("No trace was found for the requested ObservMe session id.");
|
|
265
|
+
return { scope: "session", source: "tempo", sessionId, traceId };
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
async function resolveSessionTraceId(
|
|
269
|
+
sessionId: string,
|
|
270
|
+
ctx: ObsTraceCommandContext,
|
|
271
|
+
config: ObservMeConfig,
|
|
272
|
+
options: ObsTraceSnapshotOptions,
|
|
273
|
+
): Promise<string | undefined> {
|
|
274
|
+
if (options.resolveSessionTraceId) return normalizeOptionalString(await options.resolveSessionTraceId(sessionId, ctx, config));
|
|
275
|
+
return searchSessionTraceId(config, sessionId, options);
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
async function searchSessionTraceId(
|
|
279
|
+
config: ObservMeConfig,
|
|
280
|
+
sessionId: string,
|
|
281
|
+
options: ObsTraceSnapshotOptions,
|
|
282
|
+
): Promise<string | undefined> {
|
|
283
|
+
const traces = await searchTempo(
|
|
284
|
+
config,
|
|
285
|
+
{ [COMMON_SPAN_ATTRIBUTES.PI_SESSION_ID]: sessionId },
|
|
286
|
+
createObsTraceSearchRange(options),
|
|
287
|
+
{ fetch: options.fetch },
|
|
288
|
+
);
|
|
289
|
+
|
|
290
|
+
return readFirstTraceId(traces);
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
function readFirstTraceId(traces: readonly TraceSummary[]): string | undefined {
|
|
294
|
+
return traces.map(trace => normalizeOptionalString(trace.traceId)).find(isString);
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
function createObsTraceSearchRange(options: ObsTraceSnapshotOptions): TimeRange {
|
|
298
|
+
const to = options.now?.() ?? new Date();
|
|
299
|
+
const rangeHours = normalizeSearchRangeHours(options.searchRangeHours);
|
|
300
|
+
return { from: new Date(to.getTime() - rangeHours * millisecondsPerHour), to };
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
function normalizeSearchRangeHours(value: number | undefined): number {
|
|
304
|
+
if (value === undefined || !Number.isFinite(value) || value <= 0) return DEFAULT_TRACE_SEARCH_RANGE_HOURS;
|
|
305
|
+
return value;
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
function resolveLocalTraceIdForSession(sessionId: string, session: ObsTraceSessionSnapshot): string | undefined {
|
|
309
|
+
const currentSessionId = normalizeOptionalString(session.sessionId);
|
|
310
|
+
const traceId = normalizeOptionalString(session.traceId);
|
|
311
|
+
|
|
312
|
+
if (!currentSessionId || currentSessionId !== sessionId) return undefined;
|
|
313
|
+
return traceId;
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
function normalizeObsTraceSessionId(value: string | undefined): string {
|
|
317
|
+
return normalizeObservMeSessionId(value, { emptyMessage: "Unsafe ObservMe session id: empty values are not query inputs." });
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
function normalizeOptionalString(value: string | undefined): string | undefined {
|
|
321
|
+
const trimmed = value?.trim();
|
|
322
|
+
return trimmed || undefined;
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
function normalizeTurnCount(value: number | undefined): number {
|
|
326
|
+
if (value === undefined || !Number.isFinite(value) || value < 0) return 0;
|
|
327
|
+
return Math.trunc(value);
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
function parseObsTraceOptions(tokens: readonly string[]): ParsedObsTraceRequest {
|
|
331
|
+
if (tokens.length === 0) return { request: defaultObsTraceRequest };
|
|
332
|
+
|
|
333
|
+
const state: ObsTraceParseState = { index: 0 };
|
|
334
|
+
|
|
335
|
+
while (state.index < tokens.length) {
|
|
336
|
+
const result = parseObsTraceOption(tokens, state);
|
|
337
|
+
if (result.error) return result;
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
return { request: state.request ?? defaultObsTraceRequest };
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
function parseObsTraceOption(tokens: readonly string[], state: ObsTraceParseState): ParsedObsTraceRequest {
|
|
344
|
+
const token = tokens[state.index];
|
|
345
|
+
const normalizedToken = token.toLowerCase();
|
|
346
|
+
|
|
347
|
+
if (isCurrentSessionToken(normalizedToken)) return setObsTraceRequest(state, token, defaultObsTraceRequest, state.index + 1);
|
|
348
|
+
if (normalizedToken === "--last-turn") return setObsTraceRequest(state, token, { scope: "last-turn" }, state.index + 1);
|
|
349
|
+
if (normalizedToken === "--session") return parseSeparateSessionTraceOption(tokens, state, token);
|
|
350
|
+
if (normalizedToken.startsWith("--session=")) return parseInlineSessionTraceOption(state, token);
|
|
351
|
+
return { error: unknownObsOptionMessage(token) };
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
function parseSeparateSessionTraceOption(tokens: readonly string[], state: ObsTraceParseState, token: string): ParsedObsTraceRequest {
|
|
355
|
+
const sessionId = tokens[state.index + 1];
|
|
356
|
+
if (!sessionId || sessionId.startsWith("--")) return { error: missingObsOptionValueMessage("--session") };
|
|
357
|
+
return setObsTraceRequest(state, token, { scope: "session", sessionId }, state.index + 2);
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
function parseInlineSessionTraceOption(state: ObsTraceParseState, token: string): ParsedObsTraceRequest {
|
|
361
|
+
const sessionId = token.slice("--session=".length);
|
|
362
|
+
if (!sessionId) return { error: missingObsOptionValueMessage("--session") };
|
|
363
|
+
return setObsTraceRequest(state, "--session", { scope: "session", sessionId }, state.index + 1);
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
function setObsTraceRequest(
|
|
367
|
+
state: ObsTraceParseState,
|
|
368
|
+
token: string,
|
|
369
|
+
request: ObsTraceRequest,
|
|
370
|
+
nextIndex: number,
|
|
371
|
+
): ParsedObsTraceRequest {
|
|
372
|
+
if (state.request) return { error: `Repeated or conflicting option: ${token}.` };
|
|
373
|
+
state.request = request;
|
|
374
|
+
state.index = nextIndex;
|
|
375
|
+
return {};
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
function isCurrentSessionToken(token: string): boolean {
|
|
379
|
+
return token === "--current-session";
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
function formatObsTraceScope(scope: ObsTraceScope): string {
|
|
383
|
+
if (scope === "current-session") return "current session";
|
|
384
|
+
if (scope === "last-turn") return "last turn";
|
|
385
|
+
return "session";
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
function formatObsTraceVisibilityNote(snapshot: ObsTraceSnapshot): string | undefined {
|
|
389
|
+
if (snapshot.source !== "runtime") return undefined;
|
|
390
|
+
return OBS_TRACE_ACTIVE_SESSION_NOTE;
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
function isString(value: string | undefined): value is string {
|
|
394
|
+
return value !== undefined && value.length > 0;
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
export function resolveObsTraceDiagnostic(error: unknown): ObsCommandRecoveryHint {
|
|
398
|
+
const message = readObsDiagnosticMessage(error);
|
|
399
|
+
|
|
400
|
+
if (isObsTraceSessionErrorMessage(message)) return { subsystem: "Session trace", nextAction: OBS_TRACE_SESSION_ERROR_NEXT_ACTION };
|
|
401
|
+
if (isObsTraceNotFoundMessage(message)) return { subsystem: "Tempo", nextAction: OBS_TRACE_NOT_FOUND_NEXT_ACTION };
|
|
402
|
+
return { subsystem: "Tempo", nextAction: OBS_TRACE_TEMPO_ERROR_NEXT_ACTION };
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
function isObsTraceSessionErrorMessage(message: string): boolean {
|
|
406
|
+
return /No current ObservMe session trace|No last-turn ObservMe trace|Unsafe ObservMe session id/u.test(message);
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
function isObsTraceNotFoundMessage(message: string): boolean {
|
|
410
|
+
return /No trace was found/u.test(message);
|
|
411
|
+
}
|
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import type { RegisterObsBackfillCommandOptions } from "./obs-backfill.ts";
|
|
3
|
+
import { handleObsBackfillCommand } from "./obs-backfill.ts";
|
|
4
|
+
import type { ObsAgentsCommandContext, RegisterObsAgentsCommandOptions } from "./obs-agents.ts";
|
|
5
|
+
import { handleObsAgentsCommand } from "./obs-agents.ts";
|
|
6
|
+
import type { ObsCostCommandContext, RegisterObsCostCommandOptions } from "./obs-cost.ts";
|
|
7
|
+
import { handleObsCostCommand } from "./obs-cost.ts";
|
|
8
|
+
import type { ObsErrorsCommandContext, RegisterObsErrorsCommandOptions } from "./obs-errors.ts";
|
|
9
|
+
import { handleObsErrorsCommand } from "./obs-errors.ts";
|
|
10
|
+
import type { ObsHealthCommandContext, RegisterObsHealthCommandOptions } from "./obs-health.ts";
|
|
11
|
+
import { handleObsHealthCommand } from "./obs-health.ts";
|
|
12
|
+
import type { ObsLinkCommandContext, RegisterObsLinkCommandOptions } from "./obs-link.ts";
|
|
13
|
+
import { handleObsLinkCommand } from "./obs-link.ts";
|
|
14
|
+
import type { ObsLogsCommandContext, RegisterObsLogsCommandOptions } from "./obs-logs.ts";
|
|
15
|
+
import { handleObsLogsCommand } from "./obs-logs.ts";
|
|
16
|
+
import type { ObsSessionCommandContext, RegisterObsSessionCommandOptions } from "./obs-session.ts";
|
|
17
|
+
import { handleObsSessionCommand } from "./obs-session.ts";
|
|
18
|
+
import type { ObsStatusCommandContext, RegisterObsStatusCommandOptions } from "./obs-status.ts";
|
|
19
|
+
import { handleObsStatusCommand } from "./obs-status.ts";
|
|
20
|
+
import type { ObsToolsCommandContext, RegisterObsToolsCommandOptions } from "./obs-tools.ts";
|
|
21
|
+
import { handleObsToolsCommand } from "./obs-tools.ts";
|
|
22
|
+
import type { ObsTraceCommandContext, RegisterObsTraceCommandOptions } from "./obs-trace.ts";
|
|
23
|
+
import { handleObsTraceCommand } from "./obs-trace.ts";
|
|
24
|
+
import { completeObsSubcommands, firstObsCommandToken, obsUsageWithError } from "./obs-args.ts";
|
|
25
|
+
|
|
26
|
+
export interface ObsCommandContext
|
|
27
|
+
extends ObsStatusCommandContext,
|
|
28
|
+
ObsHealthCommandContext,
|
|
29
|
+
ObsSessionCommandContext,
|
|
30
|
+
ObsCostCommandContext,
|
|
31
|
+
ObsTraceCommandContext,
|
|
32
|
+
ObsToolsCommandContext,
|
|
33
|
+
ObsAgentsCommandContext,
|
|
34
|
+
ObsErrorsCommandContext,
|
|
35
|
+
ObsLogsCommandContext,
|
|
36
|
+
ObsLinkCommandContext {}
|
|
37
|
+
|
|
38
|
+
export interface RegisterObsCommandOptions {
|
|
39
|
+
readonly status?: RegisterObsStatusCommandOptions;
|
|
40
|
+
readonly health?: RegisterObsHealthCommandOptions;
|
|
41
|
+
readonly session?: RegisterObsSessionCommandOptions;
|
|
42
|
+
readonly cost?: RegisterObsCostCommandOptions;
|
|
43
|
+
readonly trace?: RegisterObsTraceCommandOptions;
|
|
44
|
+
readonly tools?: RegisterObsToolsCommandOptions;
|
|
45
|
+
readonly agents?: RegisterObsAgentsCommandOptions;
|
|
46
|
+
readonly backfill?: RegisterObsBackfillCommandOptions;
|
|
47
|
+
readonly errors?: RegisterObsErrorsCommandOptions;
|
|
48
|
+
readonly logs?: RegisterObsLogsCommandOptions;
|
|
49
|
+
readonly link?: RegisterObsLinkCommandOptions;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const OBS_COMMAND_NAME = "obs";
|
|
53
|
+
const OBS_DEFAULT_SUBCOMMAND = "status" satisfies ObsRootSubcommandName;
|
|
54
|
+
|
|
55
|
+
type ObsRootArgsMode = "raw" | "subcommand";
|
|
56
|
+
export type ObsRootSubcommandName = keyof RegisterObsCommandOptions;
|
|
57
|
+
|
|
58
|
+
type ObsSubcommandHandler<Options> = (
|
|
59
|
+
args: string,
|
|
60
|
+
ctx: ObsCommandContext,
|
|
61
|
+
options?: Options,
|
|
62
|
+
) => Promise<void>;
|
|
63
|
+
|
|
64
|
+
type ObsSubcommandRegistry = {
|
|
65
|
+
readonly [Name in ObsRootSubcommandName]: {
|
|
66
|
+
readonly rootArgs: ObsRootArgsMode;
|
|
67
|
+
readonly handle: ObsSubcommandHandler<RegisterObsCommandOptions[Name]>;
|
|
68
|
+
readonly selectOptions: (options: RegisterObsCommandOptions) => RegisterObsCommandOptions[Name];
|
|
69
|
+
};
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
const obsSubcommandRegistry = {
|
|
73
|
+
status: {
|
|
74
|
+
rootArgs: "subcommand",
|
|
75
|
+
handle: handleObsStatusCommand,
|
|
76
|
+
selectOptions: options => options.status,
|
|
77
|
+
},
|
|
78
|
+
health: {
|
|
79
|
+
rootArgs: "subcommand",
|
|
80
|
+
handle: handleObsHealthCommand,
|
|
81
|
+
selectOptions: options => options.health,
|
|
82
|
+
},
|
|
83
|
+
session: {
|
|
84
|
+
rootArgs: "subcommand",
|
|
85
|
+
handle: handleObsSessionCommand,
|
|
86
|
+
selectOptions: options => options.session,
|
|
87
|
+
},
|
|
88
|
+
cost: {
|
|
89
|
+
rootArgs: "raw",
|
|
90
|
+
handle: handleObsCostCommand,
|
|
91
|
+
selectOptions: options => options.cost,
|
|
92
|
+
},
|
|
93
|
+
trace: {
|
|
94
|
+
rootArgs: "raw",
|
|
95
|
+
handle: handleObsTraceCommand,
|
|
96
|
+
selectOptions: options => options.trace,
|
|
97
|
+
},
|
|
98
|
+
tools: {
|
|
99
|
+
rootArgs: "raw",
|
|
100
|
+
handle: handleObsToolsCommand,
|
|
101
|
+
selectOptions: options => options.tools,
|
|
102
|
+
},
|
|
103
|
+
agents: {
|
|
104
|
+
rootArgs: "raw",
|
|
105
|
+
handle: handleObsAgentsCommand,
|
|
106
|
+
selectOptions: options => options.agents,
|
|
107
|
+
},
|
|
108
|
+
backfill: {
|
|
109
|
+
rootArgs: "raw",
|
|
110
|
+
handle: handleObsBackfillCommand,
|
|
111
|
+
selectOptions: options => options.backfill,
|
|
112
|
+
},
|
|
113
|
+
errors: {
|
|
114
|
+
rootArgs: "raw",
|
|
115
|
+
handle: handleObsErrorsCommand,
|
|
116
|
+
selectOptions: options => options.errors,
|
|
117
|
+
},
|
|
118
|
+
logs: {
|
|
119
|
+
rootArgs: "raw",
|
|
120
|
+
handle: handleObsLogsCommand,
|
|
121
|
+
selectOptions: options => options.logs,
|
|
122
|
+
},
|
|
123
|
+
link: {
|
|
124
|
+
rootArgs: "raw",
|
|
125
|
+
handle: handleObsLinkCommand,
|
|
126
|
+
selectOptions: options => options.link,
|
|
127
|
+
},
|
|
128
|
+
} as const satisfies ObsSubcommandRegistry;
|
|
129
|
+
|
|
130
|
+
const obsSubcommands = Object.keys(obsSubcommandRegistry) as ObsRootSubcommandName[];
|
|
131
|
+
const OBS_ROOT_USAGE = buildObsRootUsage(obsSubcommands);
|
|
132
|
+
|
|
133
|
+
export function registerObsCommand(pi: ExtensionAPI, options: RegisterObsCommandOptions = {}): void {
|
|
134
|
+
const command = new ObsCommand(options);
|
|
135
|
+
|
|
136
|
+
pi.registerCommand(OBS_COMMAND_NAME, {
|
|
137
|
+
description: `Run ObservMe commands. ${OBS_ROOT_USAGE}`,
|
|
138
|
+
getArgumentCompletions: getObsRootCommandArgumentCompletions,
|
|
139
|
+
handler: command.handle.bind(command),
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
export async function handleObsCommand(
|
|
144
|
+
args: string,
|
|
145
|
+
ctx: ObsCommandContext,
|
|
146
|
+
options: RegisterObsCommandOptions = {},
|
|
147
|
+
): Promise<void> {
|
|
148
|
+
const subcommand = firstObsCommandToken(args);
|
|
149
|
+
const rootSubcommand = resolveObsRootSubcommand(subcommand);
|
|
150
|
+
|
|
151
|
+
if (rootSubcommand) {
|
|
152
|
+
await dispatchObsRootSubcommand(rootSubcommand, args, ctx, options);
|
|
153
|
+
return;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
await ctx.ui?.notify?.(obsUsageWithError(OBS_ROOT_USAGE, subcommand ? `Unknown subcommand: ${subcommand}.` : undefined), "warning");
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
export function getObsRootCommandArgumentCompletions(prefix: string): Array<{ value: string; label: string }> | null {
|
|
160
|
+
return completeObsSubcommands(prefix, obsSubcommands);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
export function getObsRootSubcommands(): readonly ObsRootSubcommandName[] {
|
|
164
|
+
return [...obsSubcommands];
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
export function getObsRootUsage(): string {
|
|
168
|
+
return OBS_ROOT_USAGE;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function buildObsRootUsage(subcommands: readonly ObsRootSubcommandName[]): string {
|
|
172
|
+
return `Usage: /obs <${subcommands.join("|")}>`;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
function resolveObsRootSubcommand(subcommand: string | undefined): ObsRootSubcommandName | undefined {
|
|
176
|
+
if (!subcommand) return OBS_DEFAULT_SUBCOMMAND;
|
|
177
|
+
if (isObsRootSubcommandName(subcommand)) return subcommand;
|
|
178
|
+
return undefined;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
function isObsRootSubcommandName(value: string): value is ObsRootSubcommandName {
|
|
182
|
+
return Object.hasOwn(obsSubcommandRegistry, value);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
async function dispatchObsRootSubcommand<Name extends ObsRootSubcommandName>(
|
|
186
|
+
subcommand: Name,
|
|
187
|
+
args: string,
|
|
188
|
+
ctx: ObsCommandContext,
|
|
189
|
+
options: RegisterObsCommandOptions,
|
|
190
|
+
): Promise<void> {
|
|
191
|
+
const registration: ObsSubcommandRegistry[Name] = obsSubcommandRegistry[subcommand];
|
|
192
|
+
const handlerArgs = getObsRootHandlerArgs(subcommand, args, registration.rootArgs);
|
|
193
|
+
await registration.handle(handlerArgs, ctx, registration.selectOptions(options));
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
function getObsRootHandlerArgs(subcommand: ObsRootSubcommandName, args: string, rootArgs: ObsRootArgsMode): string {
|
|
197
|
+
if (rootArgs === "subcommand") return subcommand;
|
|
198
|
+
return args;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
class ObsCommand {
|
|
202
|
+
readonly #options: RegisterObsCommandOptions;
|
|
203
|
+
|
|
204
|
+
constructor(options: RegisterObsCommandOptions) {
|
|
205
|
+
this.#options = options;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
async handle(args: string, ctx: ObsCommandContext): Promise<void> {
|
|
209
|
+
await handleObsCommand(args, ctx, this.#options);
|
|
210
|
+
}
|
|
211
|
+
}
|