Skip to content

Commit 82f84ba

Browse files
Patrick-Erichsentingwai
authored andcommitted
feat: report failure tool (#8694)
* chore: add FAILED to status list * feat: report failure tool * add failure reporting for failed chat completions * Create status.ts * Update status.ts * sentry logigng * Update reportFailure.ts * Update posthogService.ts
1 parent 559bc06 commit 82f84ba

File tree

6 files changed

+134
-10
lines changed

6 files changed

+134
-10
lines changed

extensions/cli/src/commands/serve.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ import { createSession, getCompleteStateSnapshot } from "../session.js";
2626
import { messageQueue } from "../stream/messageQueue.js";
2727
import { constructSystemMessage } from "../systemMessage.js";
2828
import { telemetryService } from "../telemetry/telemetryService.js";
29+
import { reportFailureTool } from "../tools/reportFailure.js";
2930
import { gracefulExit } from "../util/exit.js";
3031
import { formatError } from "../util/formatError.js";
3132
import { getGitDiffSnapshot } from "../util/git.js";
@@ -472,6 +473,7 @@ export async function serve(prompt?: string, options: ServeOptions = {}) {
472473
removePartialAssistantMessage(state);
473474
} else {
474475
logger.error(`Error: ${formatError(e)}`);
476+
475477
// Add error message via ChatHistoryService
476478
const errorMessage = `Error: ${formatError(e)}`;
477479
try {
@@ -482,6 +484,18 @@ export async function serve(prompt?: string, options: ServeOptions = {}) {
482484
contextItems: [],
483485
});
484486
}
487+
488+
// Report failure to control plane (retries exhausted or non-retryable error)
489+
try {
490+
await reportFailureTool.run({
491+
errorMessage: formatError(e),
492+
});
493+
} catch (reportError) {
494+
logger.error(
495+
`Failed to report agent failure: ${formatError(reportError)}`,
496+
);
497+
// Don't block on reporting failure
498+
}
485499
}
486500
} finally {
487501
state.currentAbortController = null;

extensions/cli/src/sentry.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -95,10 +95,17 @@ class SentryService {
9595
return this.config.enabled && this.initialized;
9696
}
9797

98-
public captureException(error: Error, context?: Record<string, any>) {
98+
public captureException(
99+
error: Error,
100+
context?: Record<string, any>,
101+
level?: "info" | "warning" | "error" | "debug" | "fatal",
102+
) {
99103
if (!this.isEnabled()) return;
100104

101105
Sentry.withScope((scope) => {
106+
if (level) {
107+
scope.setLevel(level);
108+
}
102109
if (context) {
103110
Object.entries(context).forEach(([key, value]) => {
104111
scope.setContext(key, value);

extensions/cli/src/telemetry/posthogService.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -79,10 +79,16 @@ export class PosthogService {
7979
}
8080

8181
/**
82-
* - Continue user id if signed in
82+
* - Continue user id from environment (when running as agent)
83+
* - Continue user id if signed in locally
8384
* - Unique machine id if not signed in
8485
*/
8586
private getEventUserId(): string {
87+
// When running as an agent, use the user ID from the environment
88+
if (process.env.CONTINUE_USER_ID) {
89+
return process.env.CONTINUE_USER_ID;
90+
}
91+
8692
const authConfig = loadAuthConfig();
8793

8894
if (isAuthenticatedConfig(authConfig)) {

extensions/cli/src/tools/allBuiltIns.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,9 @@ import { fetchTool } from "./fetch.js";
44
import { listFilesTool } from "./listFiles.js";
55
import { multiEditTool } from "./multiEdit.js";
66
import { readFileTool } from "./readFile.js";
7+
import { reportFailureTool } from "./reportFailure.js";
78
import { runTerminalCommandTool } from "./runTerminalCommand.js";
89
import { searchCodeTool } from "./searchCode.js";
9-
import { statusTool } from "./status.js";
1010
import { writeChecklistTool } from "./writeChecklist.js";
1111
import { writeFileTool } from "./writeFile.js";
1212

@@ -22,5 +22,5 @@ export const ALL_BUILT_IN_TOOLS = [
2222
fetchTool,
2323
writeChecklistTool,
2424
exitTool,
25-
statusTool,
25+
reportFailureTool,
2626
];

extensions/cli/src/tools/index.tsx

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -25,9 +25,9 @@ import { fetchTool } from "./fetch.js";
2525
import { listFilesTool } from "./listFiles.js";
2626
import { multiEditTool } from "./multiEdit.js";
2727
import { readFileTool } from "./readFile.js";
28+
import { reportFailureTool } from "./reportFailure.js";
2829
import { runTerminalCommandTool } from "./runTerminalCommand.js";
2930
import { searchCodeTool } from "./searchCode.js";
30-
import { statusTool } from "./status.js";
3131
import {
3232
type Tool,
3333
type ToolCall,
@@ -49,6 +49,7 @@ const BASE_BUILTIN_TOOLS: Tool[] = [
4949
runTerminalCommandTool,
5050
fetchTool,
5151
writeChecklistTool,
52+
reportFailureTool,
5253
];
5354

5455
// Get all builtin tools including dynamic ones, with capability-based filtering
@@ -87,11 +88,6 @@ export async function getAllAvailableTools(
8788
tools.push(exitTool);
8889
}
8990

90-
// Add beta status tool if --beta-status-tool flag is present
91-
if (process.argv.includes("--beta-status-tool")) {
92-
tools.push(statusTool);
93-
}
94-
9591
const mcpState = await serviceContainer.get<MCPServiceState>(
9692
SERVICE_NAMES.MCP,
9793
);
Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
import { ContinueError, ContinueErrorReason } from "core/util/errors.js";
2+
3+
import { sentryService } from "../sentry.js";
4+
import {
5+
ApiRequestError,
6+
AuthenticationRequiredError,
7+
post,
8+
} from "../util/apiClient.js";
9+
import { logger } from "../util/logger.js";
10+
11+
import { Tool } from "./types.js";
12+
13+
/**
14+
* Extract the agent ID from the --id command line flag
15+
*/
16+
function getAgentIdFromArgs(): string | undefined {
17+
const args = process.argv;
18+
const idIndex = args.indexOf("--id");
19+
if (idIndex !== -1 && idIndex + 1 < args.length) {
20+
return args[idIndex + 1];
21+
}
22+
return undefined;
23+
}
24+
25+
export const reportFailureTool: Tool = {
26+
name: "ReportFailure",
27+
displayName: "Report Failure",
28+
description:
29+
"Report that the task has failed due to an unrecoverable error. Include a succinct explanation for the user.",
30+
parameters: {
31+
type: "object",
32+
required: ["errorMessage"],
33+
properties: {
34+
errorMessage: {
35+
type: "string",
36+
description: "Explain what went wrong and why you cannot continue.",
37+
},
38+
},
39+
},
40+
readonly: true,
41+
isBuiltIn: true,
42+
run: async (args: { errorMessage: string }): Promise<string> => {
43+
try {
44+
const trimmedMessage = args.errorMessage.trim();
45+
if (!trimmedMessage) {
46+
throw new ContinueError(
47+
ContinueErrorReason.Unspecified,
48+
"errorMessage is required to report a failure.",
49+
);
50+
}
51+
52+
const agentId = getAgentIdFromArgs();
53+
if (!agentId) {
54+
const errorMessage =
55+
"Agent ID is required. Please use the --id flag with cn serve.";
56+
logger.error(errorMessage);
57+
throw new ContinueError(ContinueErrorReason.Unspecified, errorMessage);
58+
}
59+
60+
// Capture failure in Sentry with context
61+
sentryService.captureException(
62+
new Error(trimmedMessage),
63+
{
64+
agent_failure: {
65+
agentId,
66+
errorMessage: trimmedMessage,
67+
},
68+
},
69+
"fatal",
70+
);
71+
72+
await post(`agents/${agentId}/status`, {
73+
status: "FAILED",
74+
errorMessage: trimmedMessage,
75+
});
76+
77+
logger.info(`Failure reported: ${trimmedMessage}`);
78+
return "Failure reported to user.";
79+
} catch (error) {
80+
if (error instanceof ContinueError) {
81+
throw error;
82+
}
83+
84+
if (error instanceof AuthenticationRequiredError) {
85+
logger.error(error.message);
86+
throw new Error("Error: Authentication required");
87+
}
88+
89+
if (error instanceof ApiRequestError) {
90+
throw new Error(
91+
`Error reporting failure: ${error.status} ${error.response || error.statusText}`,
92+
);
93+
}
94+
95+
const errorMessage =
96+
error instanceof Error ? error.message : String(error);
97+
logger.error(`Error reporting failure: ${errorMessage}`);
98+
throw new Error(`Error reporting failure: ${errorMessage}`);
99+
}
100+
},
101+
};

0 commit comments

Comments
 (0)