Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 1 addition & 7 deletions packages/junior/src/chat/app/factory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,7 @@ import {
import { createJuniorRuntimeServices } from "@/chat/app/services";
import type { JuniorRuntimeServiceOverrides } from "@/chat/app/services";
import { coerceThreadConversationState } from "@/chat/state/conversation";
import {
logException,
logWarn,
resolveErrorReference,
withSpan,
} from "@/chat/logging";
import { logException, logWarn, withSpan } from "@/chat/logging";
import { createReplyToThread } from "@/chat/runtime/reply-executor";
import {
initializeAssistantThread as initializeAssistantThreadImpl,
Expand Down Expand Up @@ -66,7 +61,6 @@ export function createSlackRuntime(
assistantUserName: botConfig.userName,
modelId: botConfig.modelId,
now: options.now ?? (() => Date.now()),
getErrorReference: resolveErrorReference,
getThreadId,
getChannelId,
getRunId,
Expand Down
24 changes: 5 additions & 19 deletions packages/junior/src/chat/logging.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1413,11 +1413,6 @@ export function setSentryScopeContext(
// High-level observability API (spans, error capture, convenience loggers)
// ---------------------------------------------------------------------------

export interface ErrorReference {
traceId: string;
eventId?: string;
}

type SpanAttributePrimitive = string | number | boolean;
type SpanAttributeValue = SpanAttributePrimitive | string[];

Expand Down Expand Up @@ -1657,21 +1652,12 @@ export function getActiveTraceId(): string | undefined {
}
}

/** Build a trace + event ID reference for error correlation. */
export function resolveErrorReference(eventId?: string): ErrorReference | null {
const traceId = getActiveTraceId();
if (!eventId && !traceId) {
return null;
}
const TURN_FAILURE_RESPONSE_TEMPLATE =
"I ran into an internal error while processing that. Reference: `event_id={eventId}`.";

if (!traceId) {
Comment thread
cursor[bot] marked this conversation as resolved.
return null;
}

return {
traceId,
...(eventId ? { eventId } : {}),
};
/** Build the static user-facing response for a failed turn. */
export function buildTurnFailureResponse(eventId: string): string {
return TURN_FAILURE_RESPONSE_TEMPLATE.replace("{eventId}", eventId);
}

// ---------------------------------------------------------------------------
Expand Down
9 changes: 0 additions & 9 deletions packages/junior/src/chat/respond-helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -227,15 +227,6 @@ export function encodeNonImageAttachmentForPrompt(attachment: {
].join("\n");
}

/** Build a user-facing message for execution failures. */
export function buildExecutionFailureMessage(toolErrorCount: number): string {
if (toolErrorCount > 0) {
return "I couldn't complete this because one or more required tools failed in this turn. I've logged the failure details.";
}

return "I couldn't complete this request in this turn due to an execution failure. I've logged the details for debugging.";
}

/** Type guard for Pi SDK tool result messages. */
export function isToolResultMessage(
value: unknown,
Expand Down
87 changes: 13 additions & 74 deletions packages/junior/src/chat/runtime/reply-executor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@ import {
type PlannedSlackReplyStage,
} from "@/chat/slack/reply";
import { buildSlackOutputMessage } from "@/chat/slack/output";
import { GEN_AI_PROVIDER_NAME } from "@/chat/pi/client";
import { generateAssistantReply as generateAssistantReplyImpl } from "@/chat/respond";
import { shouldEmitDevAgentTrace } from "@/chat/runtime/dev-agent-trace";
import {
Expand Down Expand Up @@ -62,6 +61,10 @@ import { markTurnCompleted, markTurnFailed } from "@/chat/runtime/turn";
import { startActiveTurn } from "@/chat/runtime/turn";
import { isRedundantReactionAckText } from "@/chat/services/reply-delivery-plan";
import { deleteSlackMessage } from "@/chat/slack/outbound";
import {
finalizeFailedTurnReply,
getAgentTurnDiagnosticsAttributes,
} from "@/chat/services/turn-failure-response";

export interface ReplyExecutorServices {
generateAssistantReply: typeof generateAssistantReplyImpl;
Expand All @@ -72,26 +75,6 @@ export interface ReplyExecutorServices {
) => Promise<void>;
}

function getExecutionFailureReason(reply: {
diagnostics: {
assistantMessageCount: number;
errorMessage?: string;
toolErrorCount: number;
};
}): string {
const errorMessage = reply.diagnostics.errorMessage?.trim();
if (errorMessage) {
return errorMessage;
}
if (reply.diagnostics.toolErrorCount > 0) {
return `${reply.diagnostics.toolErrorCount} tool result error(s)`;
}
if (reply.diagnostics.assistantMessageCount > 0) {
return "assistant returned no text";
}
return "empty assistant turn";
}

interface ReplyExecutorDeps {
getSlackAdapter: () => SlackAdapter;
resolveUserAttachments: (
Expand Down Expand Up @@ -302,7 +285,7 @@ export function createReplyToThread(deps: ReplyExecutorDeps) {
try {
const toolChannelId =
preparedState.artifacts.assistantContextChannelId ?? channelId;
const reply = await deps.services.generateAssistantReply(userText, {
let reply = await deps.services.generateAssistantReply(userText, {
requester: {
userId: message.author.userId,
userName: message.author.userName ?? fallbackIdentity?.userName,
Expand Down Expand Up @@ -364,59 +347,15 @@ export function createReplyToThread(deps: ReplyExecutorDeps) {
assistantUserName: botConfig.userName,
modelId: reply.diagnostics.modelId,
};
const diagnosticsAttributes = {
"gen_ai.provider.name": GEN_AI_PROVIDER_NAME,
"gen_ai.operation.name": "invoke_agent",
"app.ai.outcome": reply.diagnostics.outcome,
"app.ai.assistant_messages":
reply.diagnostics.assistantMessageCount,
"app.ai.tool_results": reply.diagnostics.toolResultCount,
"app.ai.tool_error_results": reply.diagnostics.toolErrorCount,
"app.ai.tool_call_count": reply.diagnostics.toolCalls.length,
"app.ai.used_primary_text": reply.diagnostics.usedPrimaryText,
...(reply.diagnostics.thinkingLevel
? {
"app.ai.reasoning_effort": reply.diagnostics.thinkingLevel,
}
: {}),
...(reply.diagnostics.stopReason
? {
"gen_ai.response.finish_reasons": [
reply.diagnostics.stopReason,
],
}
: {}),
...(reply.diagnostics.errorMessage
? { "error.message": reply.diagnostics.errorMessage }
: {}),
};
const diagnosticsAttributes =
getAgentTurnDiagnosticsAttributes(reply);
setSpanAttributes(diagnosticsAttributes);
if (reply.diagnostics.outcome === "provider_error") {
const providerError =
reply.diagnostics.providerError ??
new Error(
reply.diagnostics.errorMessage ??
"Provider error without explicit message",
);
logException(
providerError,
"agent_turn_provider_error",
diagnosticsContext,
diagnosticsAttributes,
"Agent turn failed with provider error",
);
} else if (reply.diagnostics.outcome !== "success") {
const failureReason = getExecutionFailureReason(reply);
logException(
new Error(`Agent turn execution failure: ${failureReason}`),
"agent_turn_execution_failure",
diagnosticsContext,
{
...diagnosticsAttributes,
"app.ai.execution_failure_reason": failureReason,
},
"Agent turn completed with execution failure",
);
if (reply.diagnostics.outcome !== "success") {
reply = finalizeFailedTurnReply({
reply,
logException,
context: diagnosticsContext,
});
}

markConversationMessage(
Expand Down
36 changes: 14 additions & 22 deletions packages/junior/src/chat/runtime/slack-runtime.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import type { Message, Thread } from "chat";
import { getSubscribedReplyPreflightDecision } from "@/chat/services/subscribed-decision";
import { isRetryableTurnError } from "@/chat/runtime/turn";
import type { ErrorReference } from "@/chat/logging";
import { buildTurnFailureResponse } from "@/chat/logging";
import { getSlackErrorObservabilityAttributes } from "@/chat/runtime/thread-context";
import type { SubscribedReplyDecision } from "@/chat/services/subscribed-reply-policy";

Expand Down Expand Up @@ -89,7 +89,6 @@ export interface SlackTurnRuntimeDependencies<TPreparedState> {
) => void;
modelId: string;
now: () => number;
getErrorReference: (eventId?: string) => ErrorReference | null;
recordSkippedSubscribedMessage: (args: {
completedAtMs: number;
decision: SubscribedReplyDecision;
Expand Down Expand Up @@ -146,16 +145,6 @@ export interface SlackTurnRuntimeDependencies<TPreparedState> {
) => Promise<void>;
}

function buildFailureMessage(reference: ErrorReference | null): string {
if (!reference) {
return "I ran into an internal error while processing that. Please try again.";
}
if (reference.eventId) {
return `I ran into an internal error while processing that. Reference: \`event_id=${reference.eventId} trace_id=${reference.traceId}\`.`;
}
return `I ran into an internal error while processing that. Reference: \`trace_id=${reference.traceId}\`.`;
}

export interface SlackTurnRuntime<
_TPreparedState,
TAssistantEvent extends AssistantLifecycleEvent = AssistantLifecycleEvent,
Expand Down Expand Up @@ -215,24 +204,21 @@ export function createSlackTurnRuntime<

const postFallbackErrorReplyWithLogging = async (args: {
thread: Thread;
reference: ErrorReference | null;
errorContext: RuntimeLogContext;
eventId?: string;
eventId: string;
postFailureEventName: string;
postFailureBody: string;
}): Promise<void> => {
try {
await args.thread.post(buildFailureMessage(args.reference));
await args.thread.post(buildTurnFailureResponse(args.eventId));
} catch (postError) {
deps.logException(
postError,
args.postFailureEventName,
args.errorContext,
{
"app.slack.reply_stage": "error_fallback_post",
...(args.eventId
? { "app.error.original_event_id": args.eventId }
: {}),
"app.error.original_event_id": args.eventId,
...getSlackErrorObservabilityAttributes(postError),
},
args.postFailureBody,
Expand Down Expand Up @@ -335,11 +321,14 @@ export function createSlackTurnRuntime<
{},
"onNewMention failed",
);
if (!eventId) {
throw new Error(
"Sentry did not return an event ID for mention_handler_failed",
);
}
await hooks?.beforeFirstResponsePost?.();
const reference = deps.getErrorReference(eventId);
await postFallbackErrorReplyWithLogging({
thread,
reference,
errorContext,
eventId,
postFailureEventName: "mention_handler_failure_reply_post_failed",
Expand Down Expand Up @@ -491,11 +480,14 @@ export function createSlackTurnRuntime<
{},
"onSubscribedMessage failed",
);
if (!eventId) {
throw new Error(
"Sentry did not return an event ID for subscribed_message_handler_failed",
);
}
await hooks?.beforeFirstResponsePost?.();
const reference = deps.getErrorReference(eventId);
await postFallbackErrorReplyWithLogging({
thread,
reference,
errorContext,
eventId,
postFailureEventName:
Expand Down
Loading
Loading