Skip to content
4 changes: 2 additions & 2 deletions packages/junior/src/chat/agent-dispatch/runner.ts
Original file line number Diff line number Diff line change
Expand Up @@ -206,8 +206,8 @@ export async function runAgentDispatchSlice(
const turnId = getDispatchTurnId(dispatch.id);
const logContext = {
conversationId,
slackThreadId: conversationId,
slackChannelId: dispatch.destination.channelId,
messageConversationId: conversationId,
destinationName: dispatch.destination.channelId,
runId: dispatch.id,
actorType: dispatch.actor.platform,
actorId: dispatch.actor.name,
Expand Down
72 changes: 43 additions & 29 deletions packages/junior/src/chat/agent/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import {
setSpanAttributes,
setTags,
summarizeMessageText,
withLogContext,
withSpan,
type LogContext,
} from "@/chat/logging";
Expand Down Expand Up @@ -185,14 +186,51 @@ export async function executeAgentRun(
request.routing.destinationVisibility ??
request.routing.slackConversation?.visibility,
});
return runWithConversationPrivacy(conversationPrivacy ?? "private", () =>
executeAgentRunInPrivacyContext(request, conversationPrivacy),
const credentialActor = request.routing.credentialContext?.actor;
const actor = actorFromRouting(request.routing);
const userActor = actor && "userId" in actor ? actor : undefined;
const runLogContext: LogContext = {
conversationId: request.conversationId,
platform: request.routing.source.platform,
messageConversationId:
request.routing.source.platform === "slack"
? request.conversationId
: request.routing.source.conversationId,
destinationName:
request.routing.destination.platform === "slack"
? request.routing.destination.channelId
: request.routing.destination.conversationId,
userId: userActor?.userId,
userName: userActor?.userName,
userEmail: userActor?.email,
runId: request.runId,
actorType: credentialActor
? "type" in credentialActor
? credentialActor.type
: "system"
: undefined,
actorId: credentialActor
? "type" in credentialActor
? credentialActor.userId
: credentialActor.name
: undefined,
assistantUserName: botConfig.userName,
};
return withLogContext(runLogContext, () =>
runWithConversationPrivacy(conversationPrivacy ?? "private", () =>
executeAgentRunInPrivacyContext(
request,
conversationPrivacy,
runLogContext,
),
),
);
}

async function executeAgentRunInPrivacyContext(
request: AgentRunRequest,
conversationPrivacy: ConversationPrivacy | undefined,
runLogContext: LogContext,
): Promise<AgentRunOutcome> {
const { conversationId, input, routing, runId, turnId } = request;
const policy = request.policy ?? {};
Expand Down Expand Up @@ -292,16 +330,7 @@ async function executeAgentRunInPrivacyContext(
activeModelProfile = projection.modelProfile;
activeModelId = modelIdForProfile(botConfig, activeModelProfile);
const shouldTrace = shouldEmitDevAgentTrace();
const spanContext: LogContext = {
conversationId,
slackThreadId: slackSource ? conversationId : undefined,
slackUserId: slackActor?.userId,
slackChannelId: slackDestination?.channelId,
runId,
...credentialActorLogContext,
assistantUserName: botConfig.userName,
modelId: activeModelId,
};
const spanContext: LogContext = { modelId: activeModelId };

// ── Skill discovery ──────────────────────────────────────────────
const availableSkills = await discoverRunSkills({
Expand Down Expand Up @@ -592,13 +621,7 @@ async function executeAgentRunInPrivacyContext(
};

setTags({
conversationId: spanContext.conversationId,
slackThreadId: slackSource ? conversationId : undefined,
slackUserId: slackActor?.userId,
slackChannelId: slackDestination?.channelId,
runId,
...credentialActorLogContext,
assistantUserName: botConfig.userName,
...runLogContext,
modelId: activeModelId,
});

Expand Down Expand Up @@ -1195,16 +1218,7 @@ async function executeAgentRunInPrivacyContext(
logException(
error,
"assistant_reply_generation_failed",
{
conversationId,
slackThreadId: slackSource ? conversationId : undefined,
slackUserId: slackActor?.userId,
slackChannelId: slackDestination?.channelId,
runId,
...credentialActorLogContext,
assistantUserName: botConfig.userName,
modelId: activeModelId,
},
{ modelId: activeModelId },
{},
"executeAgentRun failed",
);
Expand Down
14 changes: 7 additions & 7 deletions packages/junior/src/chat/ingress/slack-webhook.ts
Original file line number Diff line number Diff line change
Expand Up @@ -338,7 +338,7 @@ async function handleSlackEvent(args: {
);
} catch (error) {
logException(error, "slack_app_home_publish_failed", {
slackUserId: userId,
userId,
});
}
}
Expand Down Expand Up @@ -485,7 +485,7 @@ async function handleSlashCommandForm(args: {
await withSpan(
"chat.slash_command",
"chat.slash_command",
{ slackUserId: userId },
{ userId: userId },
async () => {
await handleSlashCommand({
adapter: args.adapter,
Expand Down Expand Up @@ -529,15 +529,15 @@ async function handleInteractivePayload(args: {
await withSpan(
"chat.app_home_disconnect",
"chat.app_home_disconnect",
{ slackUserId: userId },
{ userId: userId },
async () => {
try {
await unlinkProvider(userId, provider, args.userTokenStore);
} catch (error) {
logException(
error,
"app_home_disconnect_unlink_failed",
{ slackUserId: userId },
{ userId: userId },
{ "app.credential.provider": provider },
);
}
Expand All @@ -548,7 +548,7 @@ async function handleInteractivePayload(args: {
logException(
error,
"app_home_disconnect_publish_failed",
{ slackUserId: userId },
{ userId: userId },
{ "app.credential.provider": provider },
);
}
Expand Down Expand Up @@ -603,7 +603,7 @@ async function handleSlackForm(args: {
}),
).catch((error) => {
logException(error, "slash_command_failed", {
slackUserId: params.get("user_id") ?? undefined,
userId: params.get("user_id") ?? undefined,
});
}),
);
Expand Down Expand Up @@ -635,7 +635,7 @@ async function handleSlackForm(args: {
}),
).catch((error) => {
logException(error, "slack_interactive_payload_failed", {
slackUserId: payload.user?.id?.trim() || undefined,
userId: payload.user?.id?.trim() || undefined,
});
}),
);
Expand Down
2 changes: 1 addition & 1 deletion packages/junior/src/chat/ingress/slash-command.ts
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,7 @@ async function handleUnlink(

logInfo(
"slash_command_unlink",
{ slackUserId: actorId },
{ userId: actorId },
{ "app.credential.provider": provider },
`Unlinked ${formatProviderLabel(provider)} account via ${getCommandName()} slash command`,
);
Expand Down
136 changes: 136 additions & 0 deletions packages/junior/src/chat/log-context.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
import { AsyncLocalStorage } from "node:async_hooks";

export type LogAttributeValue = string | number | boolean | string[];
export type LogAttributes = Record<string, LogAttributeValue>;

/** Provider-neutral correlation data inherited by logs and spans in an operation. */
export interface LogContext {
conversationId?: string;
platform?: string;
requestId?: string;
messageConversationId?: string;
destinationName?: string;
userId?: string;
userName?: string;
userEmail?: string;
runId?: string;
actorType?: string;
actorId?: string;
assistantUserName?: string;
modelId?: string;
skillName?: string;
httpMethod?: string;
httpPath?: string;
urlFull?: string;
userAgent?: string;
}

/** Async attribute domain consumed directly by LogTape. */
export const logContextStorage = new AsyncLocalStorage<LogAttributes>();

/** Typed context domain retained for consumers such as native Sentry scope fields. */
const typedLogContextStorage = new AsyncLocalStorage<LogContext>();

function definedLogContext(context: LogContext): LogContext {
return Object.fromEntries(
Object.entries(context).filter(([, value]) => value !== undefined),
) as LogContext;
}

function definedAttributes(
attributes: Record<string, string | undefined>,
): LogAttributes {
return Object.fromEntries(
Object.entries(attributes).filter(
(entry): entry is [string, string] => entry[1] !== undefined,
),
);
}

/** Convert provider-neutral domain context to stable telemetry attributes. */
export function logContextToAttributes(context: LogContext): LogAttributes {
return definedAttributes({
"gen_ai.conversation.id": context.conversationId,
"app.platform": context.platform,
"app.request.id": context.requestId,
"messaging.system":
context.platform === "slack" ? "slack" : context.platform,
"messaging.message.conversation_id": context.messageConversationId,
"messaging.destination.name": context.destinationName,
"enduser.id": context.userId,
"enduser.pseudo.id": context.userName,
"app.run.id": context.runId,
"app.actor.type": context.actorType,
"app.actor.id": context.actorId,
"gen_ai.agent.name": context.assistantUserName,
"gen_ai.request.model": context.modelId,
"app.skill.name": context.skillName,
"http.request.method": context.httpMethod,
"url.path": context.httpPath,
"url.full": context.urlFull,
"user_agent.original": context.userAgent,
});
Comment thread
cursor[bot] marked this conversation as resolved.
}

/** Run an operation with merged context, restoring its parent on completion. */
export function runWithLogContext<T>(
context: LogContext,
attributes: LogAttributes,
callback: () => T,
): T {
return typedLogContextStorage.run(
{
...typedLogContextStorage.getStore(),
...definedLogContext(context),
},
() =>
logContextStorage.run(
{ ...logContextStorage.getStore(), ...attributes },
callback,
),
);
}

/** Run an operation with raw attributes, restoring its parent on completion. */
export function runWithLogAttributes<T>(
attributes: LogAttributes,
callback: () => T,
): T {
return logContextStorage.run(
{ ...logContextStorage.getStore(), ...attributes },
callback,
);
}

/** Merge raw attributes into the current scoped operation. */
export function updateLogAttributes(attributes: LogAttributes): void {
const current = logContextStorage.getStore();
if (current) {
Object.assign(current, attributes);
}
}

/** Merge context and attributes into the current scoped operation. */
export function updateLogContext(
context: LogContext,
attributes: LogAttributes,
): void {
const currentContext = typedLogContextStorage.getStore();
if (currentContext) {
Object.assign(currentContext, definedLogContext(context));
}
const currentAttributes = logContextStorage.getStore();
if (currentAttributes) {
Object.assign(currentAttributes, attributes);
}
}

/** Read the typed context bound to the current operation. */
export function getBoundLogContext(): LogContext {
return typedLogContextStorage.getStore() ?? {};
}

/** Read the attributes bound to the current operation. */
export function getBoundLogAttributes(): LogAttributes {
return logContextStorage.getStore() ?? {};
}
Loading
Loading