Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 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