mirror of
https://github.com/cline/cline.git
synced 2026-09-01 15:11:04 +08:00
fix: propagate Langfuse session telemetry (#13473)
* fix telemetry session propagation * feat telemetry client version metadata * fix(core): address Langfuse review feedback — hub client identity + delegated agent session grouping (#13475) * fix(core): rebuild hub session client identity from request headers Hub-backed sessions do not transport extensionContext (it is local-only), so the daemon's runtime built traces without the clientName/clientVersion metadata even though the hub client bakes X-CLIENT-TYPE / X-CLIENT-VERSION into the session's provider headers. Reconstruct extensionContext.client from those headers during local runtime bootstrap so hub-backed Langfuse traces carry the same client identity as local runtimes, and the daemon's header re-resolution stops clobbering the original X-CLIENT-TYPE. * fix(core): propagate parent distinctId/sessionId to delegated agents Delegated agents (spawned sub-agents, configured agents, teammates) were built without distinctId and sessionId, so their Langfuse traces had no userId or sessionId and did not group with the parent user or session. Thread the host-resolved distinctId through RuntimeBuilderInput and the root sessionId through the delegated-agent config provider, and copy both onto the delegated AgentConfig. --------- Co-authored-by: Cursor Agent <cursoragent@cursor.com> --------- Co-authored-by: Cursor Agent <cursoragent@cursor.com>
This commit is contained in:
@@ -699,6 +699,7 @@
|
||||
"@aws-sdk/credential-providers": "^3.922.0",
|
||||
"@cline/shared": "workspace:*",
|
||||
"@jerome-benoit/sap-ai-provider": "4.8.0",
|
||||
"@langfuse/core": "5.10.1",
|
||||
"@langfuse/otel": "5.10.1",
|
||||
"@langfuse/vercel-ai-sdk": "5.9.1",
|
||||
"@openrouter/ai-sdk-provider": "^3",
|
||||
|
||||
@@ -1026,6 +1026,9 @@ export class AgentRuntime {
|
||||
const usageBeforeModel = cloneUsage(this.state.usage);
|
||||
const modelRequestMetadata = omitUndefinedValues({
|
||||
distinctId: trimNonEmpty(this.config.distinctId),
|
||||
clientName: trimNonEmpty(this.config.clientName),
|
||||
clientVersion: trimNonEmpty(this.config.clientVersion),
|
||||
clineCoreVersion: trimNonEmpty(this.config.clineCoreVersion),
|
||||
sessionId: trimNonEmpty(this.config.sessionId),
|
||||
agentId: this.state.agentId,
|
||||
conversationId: trimNonEmpty(this.config.conversationId),
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import {
|
||||
buildDelegatedAgentConfig,
|
||||
createDelegatedAgentConfigProvider,
|
||||
} from "./delegated-agent";
|
||||
|
||||
describe("buildDelegatedAgentConfig", () => {
|
||||
it("inherits the parent distinctId and sessionId for telemetry grouping", () => {
|
||||
const configProvider = createDelegatedAgentConfigProvider({
|
||||
providerId: "anthropic",
|
||||
modelId: "claude-sonnet-4-5",
|
||||
distinctId: "user-123",
|
||||
sessionId: "sess-parent",
|
||||
});
|
||||
|
||||
const config = buildDelegatedAgentConfig({
|
||||
kind: "subagent",
|
||||
prompt: "review the diff",
|
||||
tools: [],
|
||||
configProvider,
|
||||
parentAgentId: "agent-lead",
|
||||
});
|
||||
|
||||
expect(config.distinctId).toBe("user-123");
|
||||
expect(config.sessionId).toBe("sess-parent");
|
||||
expect(config.parentAgentId).toBe("agent-lead");
|
||||
});
|
||||
|
||||
it("leaves identity fields undefined when the parent has none", () => {
|
||||
const configProvider = createDelegatedAgentConfigProvider({
|
||||
providerId: "anthropic",
|
||||
modelId: "claude-sonnet-4-5",
|
||||
});
|
||||
|
||||
const config = buildDelegatedAgentConfig({
|
||||
kind: "subagent",
|
||||
prompt: "review the diff",
|
||||
tools: [],
|
||||
configProvider,
|
||||
});
|
||||
|
||||
expect(config.distinctId).toBeUndefined();
|
||||
expect(config.sessionId).toBeUndefined();
|
||||
});
|
||||
});
|
||||
@@ -46,6 +46,16 @@ export interface DelegatedAgentRuntimeConfig
|
||||
logger?: BasicLogger;
|
||||
telemetry?: ITelemetryService;
|
||||
workspaceMetadata?: string;
|
||||
/**
|
||||
* Stable end-user identity inherited from the parent session so
|
||||
* delegated-agent telemetry (Langfuse `userId`) groups with the user.
|
||||
*/
|
||||
distinctId?: string;
|
||||
/**
|
||||
* Root core session id inherited from the parent session so
|
||||
* delegated-agent telemetry (Langfuse `sessionId`) groups with it.
|
||||
*/
|
||||
sessionId?: string;
|
||||
}
|
||||
|
||||
export interface DelegatedAgentConfigProvider {
|
||||
@@ -118,6 +128,8 @@ export function buildDelegatedAgentConfig(
|
||||
|
||||
return {
|
||||
...options.configProvider.getConnectionConfig(),
|
||||
distinctId: runtimeConfig.distinctId,
|
||||
sessionId: runtimeConfig.sessionId,
|
||||
systemPrompt,
|
||||
tools: options.tools,
|
||||
maxIterations: options.maxIterations ?? runtimeConfig.maxIterations,
|
||||
|
||||
@@ -13,6 +13,7 @@ import type {
|
||||
ITelemetryService,
|
||||
} from "@cline/shared";
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
import { version as clineCoreVersion } from "../../../package.json";
|
||||
import {
|
||||
buildMessageModelInfo,
|
||||
buildModelOptions,
|
||||
@@ -192,6 +193,32 @@ describe("createAgentRuntimeConfig", () => {
|
||||
);
|
||||
});
|
||||
|
||||
it("maps telemetry identity fields from AgentConfig", () => {
|
||||
const runtimeConfig = createAgentRuntimeConfig({
|
||||
agentConfig: makeAgentConfig({
|
||||
distinctId: "user-123",
|
||||
extensionContext: {
|
||||
client: { name: "cline-cli", version: "3.0.38" },
|
||||
},
|
||||
}),
|
||||
agentId: "a",
|
||||
model: nullModel,
|
||||
});
|
||||
expect(runtimeConfig.distinctId).toBe("user-123");
|
||||
expect(runtimeConfig.clientName).toBe("cline-cli");
|
||||
expect(runtimeConfig.clientVersion).toBe("3.0.38");
|
||||
expect(runtimeConfig.clineCoreVersion).toBe(clineCoreVersion);
|
||||
});
|
||||
|
||||
it("falls back to AgentConfig.sessionId when the input has none", () => {
|
||||
const runtimeConfig = createAgentRuntimeConfig({
|
||||
agentConfig: makeAgentConfig({ sessionId: "sess-parent" }),
|
||||
agentId: "a",
|
||||
model: nullModel,
|
||||
});
|
||||
expect(runtimeConfig.sessionId).toBe("sess-parent");
|
||||
});
|
||||
|
||||
it("uses the override systemPrompt when provided", () => {
|
||||
const runtimeConfig = createAgentRuntimeConfig({
|
||||
agentConfig: makeAgentConfig({ systemPrompt: "default" }),
|
||||
|
||||
@@ -25,6 +25,7 @@ import type {
|
||||
BasicLogger,
|
||||
ITelemetryService,
|
||||
} from "@cline/shared";
|
||||
import { version as clineCoreVersion } from "../../../package.json";
|
||||
|
||||
/**
|
||||
* Inputs required to assemble an `AgentRuntimeConfig`. Distinct from
|
||||
@@ -96,6 +97,10 @@ export function createAgentRuntimeConfig(
|
||||
const toolExecution = resolveToolExecution(agentConfig.maxParallelToolCalls);
|
||||
|
||||
const config: AgentRuntimeConfig = {
|
||||
distinctId: agentConfig.distinctId,
|
||||
clientName: agentConfig.extensionContext?.client?.name,
|
||||
clientVersion: agentConfig.extensionContext?.client?.version,
|
||||
clineCoreVersion,
|
||||
sessionId: input.sessionId ?? agentConfig.sessionId,
|
||||
agentId: input.agentId,
|
||||
conversationId: input.conversationId,
|
||||
|
||||
@@ -264,6 +264,7 @@ export class LocalRuntimeHost implements RuntimeHost {
|
||||
private readonly providerSettingsManager: ProviderSettingsManager;
|
||||
private readonly oauthTokenManager: RuntimeOAuthTokenManager;
|
||||
private readonly defaultTelemetry?: ITelemetryService;
|
||||
private readonly distinctId: string;
|
||||
private readonly defaultLogger?: BasicLogger;
|
||||
private readonly defaultFetch?: typeof fetch;
|
||||
private readonly events = new RuntimeHostEventBus();
|
||||
@@ -286,6 +287,7 @@ export class LocalRuntimeHost implements RuntimeHost {
|
||||
const homeDir = homedir();
|
||||
if (homeDir) setHomeDirIfUnset(homeDir);
|
||||
const distinctId = resolveCoreDistinctId(options.distinctId);
|
||||
this.distinctId = distinctId;
|
||||
this.sessionService = options.sessionService;
|
||||
this.runtimeBuilder = options.runtimeBuilder ?? new DefaultRuntimeBuilder();
|
||||
this.createAgentInstance =
|
||||
@@ -612,6 +614,7 @@ export class LocalRuntimeHost implements RuntimeHost {
|
||||
if (!resumedArtifacts) manifest.metadata = initialSessionMetadata;
|
||||
const runtime = await this.runtimeBuilder.build({
|
||||
...bootstrap.runtimeBuilderInput,
|
||||
distinctId: this.distinctId,
|
||||
runCommandExecutionController: this.runCommandExecutionController,
|
||||
});
|
||||
const configWithProvider = bootstrap.config;
|
||||
@@ -712,6 +715,7 @@ export class LocalRuntimeHost implements RuntimeHost {
|
||||
});
|
||||
|
||||
const agentConfig = {
|
||||
distinctId: this.distinctId,
|
||||
sessionId,
|
||||
providerId: providerConfig.providerId,
|
||||
modelId: providerConfig.modelId,
|
||||
|
||||
@@ -539,6 +539,8 @@ export class DefaultRuntimeBuilder implements RuntimeBuilder {
|
||||
const delegatedAgentConfigProvider = createDelegatedAgentConfigProvider({
|
||||
providerId: config.providerId,
|
||||
modelId: config.modelId,
|
||||
distinctId: input.distinctId,
|
||||
sessionId: config.sessionId,
|
||||
cwd: config.cwd,
|
||||
apiKey: config.apiKey ?? "",
|
||||
baseUrl: config.baseUrl,
|
||||
|
||||
@@ -56,6 +56,11 @@ export interface BuiltRuntime {
|
||||
|
||||
export interface RuntimeBuilderInput {
|
||||
config: CoreSessionConfig;
|
||||
/**
|
||||
* Host-resolved stable end-user identity, forwarded so delegated agents
|
||||
* (sub-agents / teammates) emit the same telemetry `userId` as the lead.
|
||||
*/
|
||||
distinctId?: string;
|
||||
hooks?: AgentHooks;
|
||||
extensions?: AgentConfig["extensions"];
|
||||
onTeamEvent?: (event: TeamEvent) => void;
|
||||
|
||||
@@ -474,6 +474,82 @@ describe("prepareLocalRuntimeBootstrap", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("rebuilds extensionContext.client from hub-baked request headers", async () => {
|
||||
const { prepareLocalRuntimeBootstrap } = await import(
|
||||
"./local-runtime-bootstrap"
|
||||
);
|
||||
|
||||
const input = createStartInput();
|
||||
const config = input.config as typeof input.config & {
|
||||
headers: Record<string, string>;
|
||||
};
|
||||
config.headers = {
|
||||
"X-CLIENT-TYPE": "cline-cli",
|
||||
"X-CLIENT-VERSION": "3.0.38",
|
||||
};
|
||||
|
||||
const bootstrap = await prepareLocalRuntimeBootstrap({
|
||||
input,
|
||||
sessionId: "sess-hub-client",
|
||||
providerSettingsManager: createProviderSettingsManager() as never,
|
||||
defaultTelemetry: undefined,
|
||||
defaultToolPolicies: undefined,
|
||||
onPluginEvent: () => {},
|
||||
onTeamEvent: () => {},
|
||||
createSpawnTool,
|
||||
readSessionMetadata: async () => undefined,
|
||||
writeSessionMetadata: async () => {},
|
||||
});
|
||||
|
||||
expect(bootstrap.config.extensionContext?.client).toEqual({
|
||||
name: "cline-cli",
|
||||
version: "3.0.38",
|
||||
});
|
||||
expect(bootstrap.providerConfig.headers).toMatchObject({
|
||||
"User-Agent": "Cline/3.0.38",
|
||||
"X-CLIENT-TYPE": "cline-cli",
|
||||
"X-CLIENT-VERSION": "3.0.38",
|
||||
});
|
||||
});
|
||||
|
||||
it("prefers configured extensionContext.client over header-derived identity", async () => {
|
||||
const { prepareLocalRuntimeBootstrap } = await import(
|
||||
"./local-runtime-bootstrap"
|
||||
);
|
||||
|
||||
const input = createStartInput();
|
||||
const config = input.config as typeof input.config & {
|
||||
headers: Record<string, string>;
|
||||
};
|
||||
config.headers = {
|
||||
"X-CLIENT-TYPE": "header-client",
|
||||
"X-CLIENT-VERSION": "0.0.1",
|
||||
};
|
||||
|
||||
const bootstrap = await prepareLocalRuntimeBootstrap({
|
||||
input,
|
||||
localRuntime: {
|
||||
extensionContext: {
|
||||
client: { name: "cline-vscode", version: "9.9.9" },
|
||||
},
|
||||
},
|
||||
sessionId: "sess-local-client",
|
||||
providerSettingsManager: createProviderSettingsManager() as never,
|
||||
defaultTelemetry: undefined,
|
||||
defaultToolPolicies: undefined,
|
||||
onPluginEvent: () => {},
|
||||
onTeamEvent: () => {},
|
||||
createSpawnTool,
|
||||
readSessionMetadata: async () => undefined,
|
||||
writeSessionMetadata: async () => {},
|
||||
});
|
||||
|
||||
expect(bootstrap.config.extensionContext?.client).toEqual({
|
||||
name: "cline-vscode",
|
||||
version: "9.9.9",
|
||||
});
|
||||
});
|
||||
|
||||
it("uses host request headers for Cline providers on core sessions", async () => {
|
||||
const { prepareLocalRuntimeBootstrap } = await import(
|
||||
"./local-runtime-bootstrap"
|
||||
|
||||
@@ -5,6 +5,7 @@ import type {
|
||||
AgentHooks,
|
||||
AgentTool,
|
||||
BasicLogger,
|
||||
ClientContext,
|
||||
ExtensionContext,
|
||||
ITelemetryService,
|
||||
RuntimeConfigExtensionKind,
|
||||
@@ -93,6 +94,25 @@ function logPluginDiagnostics(
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Recover client identity from the Cline request headers baked into the
|
||||
* session config. Hub-backed sessions do not transport `extensionContext`
|
||||
* (it is local-only), but the hub client resolves `X-CLIENT-TYPE` /
|
||||
* `X-CLIENT-VERSION` headers before `session.create`, so the daemon can
|
||||
* rebuild `extensionContext.client` from them and keep trace metadata
|
||||
* (Langfuse `clientName` / `clientVersion`) consistent with local runtimes.
|
||||
*/
|
||||
function resolveClientContextFromHeaders(
|
||||
headers: Record<string, string> | undefined,
|
||||
): ClientContext | undefined {
|
||||
const name = headers?.["X-CLIENT-TYPE"]?.trim();
|
||||
if (!name) {
|
||||
return undefined;
|
||||
}
|
||||
const version = headers?.["X-CLIENT-VERSION"]?.trim();
|
||||
return { name, ...(version ? { version } : {}) };
|
||||
}
|
||||
|
||||
function resolveReasoningSettings(
|
||||
config: CoreSessionConfig,
|
||||
storedReasoning: ProviderSettings["reasoning"],
|
||||
@@ -287,8 +307,12 @@ export async function prepareLocalRuntimeBootstrap(
|
||||
initError,
|
||||
} = await buildWorkspaceMetadataWithInfo(workspacePath);
|
||||
const configuredExtensionContext = localConfig?.extensionContext;
|
||||
const headerClientContext = configuredExtensionContext?.client
|
||||
? undefined
|
||||
: resolveClientContextFromHeaders(input.config.headers);
|
||||
const extensionContext: ExtensionContext = {
|
||||
...(configuredExtensionContext ?? {}),
|
||||
...(headerClientContext ? { client: headerClientContext } : {}),
|
||||
workspace: {
|
||||
...workspaceInfo,
|
||||
...(configuredExtensionContext?.workspace ?? {}),
|
||||
|
||||
@@ -64,6 +64,7 @@
|
||||
"@aws-sdk/credential-providers": "^3.922.0",
|
||||
"@cline/shared": "workspace:*",
|
||||
"@jerome-benoit/sap-ai-provider": "4.8.0",
|
||||
"@langfuse/core": "5.10.1",
|
||||
"@langfuse/otel": "5.10.1",
|
||||
"@langfuse/vercel-ai-sdk": "5.9.1",
|
||||
"@openrouter/ai-sdk-provider": "^3",
|
||||
|
||||
@@ -599,6 +599,50 @@ async function ensureGatewayLangfuseTelemetry(
|
||||
}
|
||||
}
|
||||
|
||||
async function withAiSdkLangfuseTraceContext<T>(
|
||||
enabled: boolean,
|
||||
request: GatewayStreamRequest,
|
||||
callback: () => T | Promise<T>,
|
||||
): Promise<T> {
|
||||
const metadata =
|
||||
request.metadata && typeof request.metadata === "object"
|
||||
? request.metadata
|
||||
: {};
|
||||
const tags = Array.isArray(metadata.tags)
|
||||
? metadata.tags.filter(
|
||||
(value): value is string =>
|
||||
typeof value === "string" && value.trim().length > 0,
|
||||
)
|
||||
: undefined;
|
||||
const distinctId =
|
||||
typeof metadata.distinctId === "string" ? metadata.distinctId : undefined;
|
||||
const sessionId =
|
||||
typeof metadata.sessionId === "string" ? metadata.sessionId : undefined;
|
||||
|
||||
if (!enabled || (!distinctId && !sessionId && !tags?.length)) {
|
||||
return await callback();
|
||||
}
|
||||
|
||||
const runtime = await import("../services/langfuse-telemetry");
|
||||
return await runtime.withLangfuseTraceAttributes(
|
||||
true,
|
||||
{
|
||||
...(distinctId ? { userId: distinctId } : {}),
|
||||
...(sessionId ? { sessionId } : {}),
|
||||
...(tags?.length ? { tags } : {}),
|
||||
metadata: {
|
||||
...(typeof metadata.conversationId === "string"
|
||||
? { conversationId: metadata.conversationId }
|
||||
: {}),
|
||||
...(typeof metadata.runId === "string"
|
||||
? { runId: metadata.runId }
|
||||
: {}),
|
||||
},
|
||||
},
|
||||
callback,
|
||||
);
|
||||
}
|
||||
|
||||
function buildAiSdkRuntimeContext(
|
||||
request: GatewayStreamRequest,
|
||||
context: GatewayProviderContext,
|
||||
@@ -625,6 +669,15 @@ function buildAiSdkRuntimeContext(
|
||||
...(typeof metadata.sessionId === "string"
|
||||
? { sessionId: metadata.sessionId }
|
||||
: {}),
|
||||
...(typeof metadata.clientName === "string"
|
||||
? { clientName: metadata.clientName }
|
||||
: {}),
|
||||
...(typeof metadata.clientVersion === "string"
|
||||
? { clientVersion: metadata.clientVersion }
|
||||
: {}),
|
||||
...(typeof metadata.clineCoreVersion === "string"
|
||||
? { clineCoreVersion: metadata.clineCoreVersion }
|
||||
: {}),
|
||||
...(tags && tags.length > 0 ? { tags } : {}),
|
||||
// Keep Cline correlation fields available even when the integration
|
||||
// does not promote them to first-class Langfuse fields.
|
||||
@@ -2152,71 +2205,79 @@ function createAiSdkProvider(kind: ProviderModuleKind): GatewayProviderFactory {
|
||||
...(portableReasoning ? { reasoning: portableReasoning } : {}),
|
||||
},
|
||||
});
|
||||
stream = streamText({
|
||||
model: withEmptyResponseRetry(
|
||||
provider.operations.language(context.model.id),
|
||||
provider.retryEmptyResponses,
|
||||
context.logger,
|
||||
) as never,
|
||||
messages: messages as never,
|
||||
...(useSystemOption ? { system: systemPrompt } : {}),
|
||||
...(tools ? { tools } : {}),
|
||||
abortSignal: request.signal,
|
||||
experimental_repairToolCall: repairMalformedToolCall as never,
|
||||
experimental_telemetry: {
|
||||
isEnabled: langfuse,
|
||||
functionId: "cline-agent-turn",
|
||||
includeRuntimeContext: {
|
||||
distinctId: true,
|
||||
userId: true,
|
||||
sessionId: true,
|
||||
tags: true,
|
||||
conversationId: true,
|
||||
runId: true,
|
||||
iteration: true,
|
||||
providerId: true,
|
||||
modelId: true,
|
||||
resolvedModelId: true,
|
||||
},
|
||||
},
|
||||
runtimeContext: buildAiSdkRuntimeContext(request, context),
|
||||
providerOptions: providerOptions as never,
|
||||
...(provider.executesModelTools && activeModelTools.length
|
||||
? { stopWhen: stepCountIs(8) }
|
||||
: {}),
|
||||
...requestConfig,
|
||||
...(portableReasoning ? { reasoning: portableReasoning } : {}),
|
||||
onError: ({ error: streamError }) => {
|
||||
const captured = captureStreamError(streamError);
|
||||
const msg = captured.message;
|
||||
capturedError.current = captured;
|
||||
if (log?.error) {
|
||||
log.error("[ai-sdk] stream error", {
|
||||
providerId: request.providerId,
|
||||
error: streamError,
|
||||
severity: "error",
|
||||
});
|
||||
} else if (log) {
|
||||
log.log(`[ai-sdk] stream error: ${msg}`, {
|
||||
providerId: request.providerId,
|
||||
severity: "error",
|
||||
});
|
||||
}
|
||||
captured.reported = captureSdkError(context.telemetry, {
|
||||
component: "llms",
|
||||
operation: "provider.stream",
|
||||
error: streamError,
|
||||
errorMessage: msg,
|
||||
severity: "error",
|
||||
handled: true,
|
||||
context: {
|
||||
providerId: request.providerId,
|
||||
modelId: request.modelId,
|
||||
providerKind: kind,
|
||||
stream = await withAiSdkLangfuseTraceContext(
|
||||
langfuse,
|
||||
request,
|
||||
() =>
|
||||
streamText({
|
||||
model: withEmptyResponseRetry(
|
||||
provider.operations.language(context.model.id),
|
||||
provider.retryEmptyResponses,
|
||||
context.logger,
|
||||
) as never,
|
||||
messages: messages as never,
|
||||
...(useSystemOption ? { system: systemPrompt } : {}),
|
||||
...(tools ? { tools } : {}),
|
||||
abortSignal: request.signal,
|
||||
experimental_repairToolCall: repairMalformedToolCall as never,
|
||||
experimental_telemetry: {
|
||||
isEnabled: langfuse,
|
||||
functionId: "cline-agent-turn",
|
||||
includeRuntimeContext: {
|
||||
distinctId: true,
|
||||
userId: true,
|
||||
sessionId: true,
|
||||
clientName: true,
|
||||
clientVersion: true,
|
||||
clineCoreVersion: true,
|
||||
tags: true,
|
||||
conversationId: true,
|
||||
runId: true,
|
||||
iteration: true,
|
||||
providerId: true,
|
||||
modelId: true,
|
||||
resolvedModelId: true,
|
||||
},
|
||||
},
|
||||
});
|
||||
},
|
||||
}) as unknown as AiSdkStreamResult;
|
||||
runtimeContext: buildAiSdkRuntimeContext(request, context),
|
||||
providerOptions: providerOptions as never,
|
||||
...(provider.executesModelTools && activeModelTools.length
|
||||
? { stopWhen: stepCountIs(8) }
|
||||
: {}),
|
||||
...requestConfig,
|
||||
...(portableReasoning ? { reasoning: portableReasoning } : {}),
|
||||
onError: ({ error: streamError }) => {
|
||||
const captured = captureStreamError(streamError);
|
||||
const msg = captured.message;
|
||||
capturedError.current = captured;
|
||||
if (log?.error) {
|
||||
log.error("[ai-sdk] stream error", {
|
||||
providerId: request.providerId,
|
||||
error: streamError,
|
||||
severity: "error",
|
||||
});
|
||||
} else if (log) {
|
||||
log.log(`[ai-sdk] stream error: ${msg}`, {
|
||||
providerId: request.providerId,
|
||||
severity: "error",
|
||||
});
|
||||
}
|
||||
captured.reported = captureSdkError(context.telemetry, {
|
||||
component: "llms",
|
||||
operation: "provider.stream",
|
||||
error: streamError,
|
||||
errorMessage: msg,
|
||||
severity: "error",
|
||||
handled: true,
|
||||
context: {
|
||||
providerId: request.providerId,
|
||||
modelId: request.modelId,
|
||||
providerKind: kind,
|
||||
},
|
||||
});
|
||||
},
|
||||
}) as unknown as AiSdkStreamResult,
|
||||
);
|
||||
|
||||
// Suppress dangling promise rejections (finishReason, totalUsage, steps, etc.)
|
||||
// BEFORE iterating. The AI SDK rejects these DelayedPromises inside the stream's
|
||||
|
||||
@@ -11,6 +11,32 @@ type LangfuseTelemetryConfig = {
|
||||
secretKey: string;
|
||||
};
|
||||
|
||||
export type LangfuseTraceAttributes = {
|
||||
userId?: string;
|
||||
sessionId?: string;
|
||||
tags?: string[];
|
||||
metadata?: Record<string, string>;
|
||||
traceName?: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* Set Langfuse trace-level attributes for the duration of an SDK operation.
|
||||
* Runtime context is useful observation metadata, but Langfuse's Sessions and
|
||||
* Users views are indexed from propagated trace attributes instead.
|
||||
*/
|
||||
export async function withLangfuseTraceAttributes<T>(
|
||||
enabled: boolean,
|
||||
attributes: LangfuseTraceAttributes,
|
||||
callback: () => T | Promise<T>,
|
||||
): Promise<T> {
|
||||
if (!enabled) {
|
||||
return await callback();
|
||||
}
|
||||
|
||||
const { propagateAttributes } = await import("@langfuse/core");
|
||||
return await propagateAttributes(attributes, callback);
|
||||
}
|
||||
|
||||
const LANGFUSE_DEBUG_ENV = "CLINE_DEBUG_LANGFUSE";
|
||||
|
||||
let langfuseTelemetryReady: boolean | undefined;
|
||||
|
||||
@@ -481,6 +481,12 @@ export interface AgentRuntimeConfig {
|
||||
* This is intentionally separate from the host-owned session id.
|
||||
*/
|
||||
distinctId?: string;
|
||||
/** Calling client surface, for example `cline-vscode` or `cline-sdk`. */
|
||||
clientName?: string;
|
||||
/** Calling client version, such as the VS Code extension version. */
|
||||
clientVersion?: string;
|
||||
/** Version of the Cline Core SDK executing the runtime. */
|
||||
clineCoreVersion?: string;
|
||||
/**
|
||||
* Core/hub runtime session identifier.
|
||||
*
|
||||
|
||||
@@ -684,6 +684,8 @@ export const AgentResultSchema = z.object({
|
||||
* Configuration for creating an Agent
|
||||
*/
|
||||
export interface AgentConfig {
|
||||
/** Stable end-user identity used for provider and observability metadata. */
|
||||
distinctId?: string;
|
||||
/**
|
||||
* Core/hub runtime session identifier.
|
||||
*
|
||||
@@ -911,6 +913,7 @@ export interface AgentConfig {
|
||||
}
|
||||
|
||||
export const AgentConfigSchema = z.object({
|
||||
distinctId: z.string().optional(),
|
||||
sessionId: z.string().optional(),
|
||||
// Provider Settings
|
||||
providerId: z.string(),
|
||||
|
||||
Reference in New Issue
Block a user