Add Claude observability tracing and diagnostics UI
This commit is contained in:
@@ -10,6 +10,7 @@ import { isDomainEventType, type DomainEventEmission } from "../agents/domain-ev
|
||||
import type { ActorExecutionInput, ActorExecutionResult, ActorExecutor } from "../agents/pipeline.js";
|
||||
import { isRecord, type JsonObject, type JsonValue } from "../agents/types.js";
|
||||
import { createSessionContext, type SessionContext } from "../examples/session-context.js";
|
||||
import { ClaudeObservabilityLogger } from "./claude-observability.js";
|
||||
|
||||
export type RunProvider = "codex" | "claude";
|
||||
|
||||
@@ -17,6 +18,7 @@ export type ProviderRunRuntime = {
|
||||
provider: RunProvider;
|
||||
config: Readonly<AppConfig>;
|
||||
sessionContext: SessionContext;
|
||||
claudeObservability: ClaudeObservabilityLogger;
|
||||
close: () => Promise<void>;
|
||||
};
|
||||
|
||||
@@ -416,6 +418,40 @@ type ClaudeTurnResult = {
|
||||
usage: ProviderUsage;
|
||||
};
|
||||
|
||||
function toClaudeTraceContext(actorInput: ActorExecutionInput): {
|
||||
sessionId: string;
|
||||
nodeId: string;
|
||||
attempt: number;
|
||||
depth: number;
|
||||
} {
|
||||
return {
|
||||
sessionId: actorInput.sessionId,
|
||||
nodeId: actorInput.node.id,
|
||||
attempt: actorInput.attempt,
|
||||
depth: actorInput.depth,
|
||||
};
|
||||
}
|
||||
|
||||
function toProviderUsageJson(usage: ProviderUsage): JsonObject {
|
||||
const output: JsonObject = {};
|
||||
if (typeof usage.tokenInput === "number") {
|
||||
output.tokenInput = usage.tokenInput;
|
||||
}
|
||||
if (typeof usage.tokenOutput === "number") {
|
||||
output.tokenOutput = usage.tokenOutput;
|
||||
}
|
||||
if (typeof usage.tokenTotal === "number") {
|
||||
output.tokenTotal = usage.tokenTotal;
|
||||
}
|
||||
if (typeof usage.durationMs === "number") {
|
||||
output.durationMs = usage.durationMs;
|
||||
}
|
||||
if (typeof usage.costUsd === "number") {
|
||||
output.costUsd = usage.costUsd;
|
||||
}
|
||||
return output;
|
||||
}
|
||||
|
||||
function buildClaudeOptions(input: {
|
||||
runtime: ProviderRunRuntime;
|
||||
actorInput: ActorExecutionInput;
|
||||
@@ -433,6 +469,7 @@ function buildClaudeOptions(input: {
|
||||
...runtime.sessionContext.runtimeInjection.env,
|
||||
...buildClaudeAuthEnv(runtime.config.provider),
|
||||
};
|
||||
const traceContext = toClaudeTraceContext(actorInput);
|
||||
|
||||
return {
|
||||
maxTurns: CLAUDE_PROVIDER_MAX_TURNS,
|
||||
@@ -449,6 +486,9 @@ function buildClaudeOptions(input: {
|
||||
canUseTool: actorInput.mcp.createClaudeCanUseTool(),
|
||||
cwd: runtime.sessionContext.runtimeInjection.workingDirectory,
|
||||
env: runtimeEnv,
|
||||
...runtime.claudeObservability.toOptionOverrides({
|
||||
context: traceContext,
|
||||
}),
|
||||
outputFormat: CLAUDE_OUTPUT_FORMAT,
|
||||
};
|
||||
}
|
||||
@@ -458,10 +498,19 @@ async function runClaudeTurn(input: {
|
||||
actorInput: ActorExecutionInput;
|
||||
prompt: string;
|
||||
}): Promise<ClaudeTurnResult> {
|
||||
const traceContext = toClaudeTraceContext(input.actorInput);
|
||||
const options = buildClaudeOptions({
|
||||
runtime: input.runtime,
|
||||
actorInput: input.actorInput,
|
||||
});
|
||||
input.runtime.claudeObservability.recordQueryStarted({
|
||||
context: traceContext,
|
||||
data: {
|
||||
...(options.model ? { model: options.model } : {}),
|
||||
maxTurns: options.maxTurns ?? CLAUDE_PROVIDER_MAX_TURNS,
|
||||
cwd: input.runtime.sessionContext.runtimeInjection.workingDirectory,
|
||||
},
|
||||
});
|
||||
|
||||
const startedAt = Date.now();
|
||||
const stream = query({
|
||||
@@ -472,6 +521,7 @@ async function runClaudeTurn(input: {
|
||||
let resultText = "";
|
||||
let structuredOutput: unknown;
|
||||
let usage: ProviderUsage = {};
|
||||
let messageCount = 0;
|
||||
|
||||
const onAbort = (): void => {
|
||||
stream.close();
|
||||
@@ -481,6 +531,12 @@ async function runClaudeTurn(input: {
|
||||
|
||||
try {
|
||||
for await (const message of stream as AsyncIterable<SDKMessage>) {
|
||||
messageCount += 1;
|
||||
input.runtime.claudeObservability.recordMessage({
|
||||
context: traceContext,
|
||||
message,
|
||||
});
|
||||
|
||||
if (message.type !== "result") {
|
||||
continue;
|
||||
}
|
||||
@@ -502,6 +558,12 @@ async function runClaudeTurn(input: {
|
||||
costUsd: message.total_cost_usd,
|
||||
};
|
||||
}
|
||||
} catch (error) {
|
||||
input.runtime.claudeObservability.recordQueryError({
|
||||
context: traceContext,
|
||||
error,
|
||||
});
|
||||
throw error;
|
||||
} finally {
|
||||
input.actorInput.signal.removeEventListener("abort", onAbort);
|
||||
stream.close();
|
||||
@@ -512,9 +574,22 @@ async function runClaudeTurn(input: {
|
||||
}
|
||||
|
||||
if (!resultText) {
|
||||
throw new Error("Claude run completed without a final result.");
|
||||
const error = new Error("Claude run completed without a final result.");
|
||||
input.runtime.claudeObservability.recordQueryError({
|
||||
context: traceContext,
|
||||
error,
|
||||
});
|
||||
throw error;
|
||||
}
|
||||
|
||||
input.runtime.claudeObservability.recordQueryCompleted({
|
||||
context: traceContext,
|
||||
data: {
|
||||
messageCount,
|
||||
usage: toProviderUsageJson(usage),
|
||||
},
|
||||
});
|
||||
|
||||
return {
|
||||
text: resultText,
|
||||
structuredOutput,
|
||||
@@ -554,19 +629,29 @@ export async function createProviderRunRuntime(input: {
|
||||
initialPrompt: string;
|
||||
config: Readonly<AppConfig>;
|
||||
projectPath: string;
|
||||
observabilityRootPath?: string;
|
||||
}): Promise<ProviderRunRuntime> {
|
||||
const sessionContext = await createSessionContext(input.provider, {
|
||||
prompt: input.initialPrompt,
|
||||
config: input.config,
|
||||
workspaceRoot: input.projectPath,
|
||||
});
|
||||
const claudeObservability = new ClaudeObservabilityLogger({
|
||||
workspaceRoot: input.observabilityRootPath ?? input.projectPath,
|
||||
config: input.config.provider.claudeObservability,
|
||||
});
|
||||
|
||||
return {
|
||||
provider: input.provider,
|
||||
config: input.config,
|
||||
sessionContext,
|
||||
claudeObservability,
|
||||
close: async () => {
|
||||
await sessionContext.close();
|
||||
try {
|
||||
await sessionContext.close();
|
||||
} finally {
|
||||
await claudeObservability.close();
|
||||
}
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user