Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
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
2 changes: 2 additions & 0 deletions extensions/cli/src/tools/allBuiltIns.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { editTool } from "./edit.js";
import { eventTool } from "./event.js";
import { exitTool } from "./exit.js";
import { fetchTool } from "./fetch.js";
import { listFilesTool } from "./listFiles.js";
Expand All @@ -25,4 +26,5 @@ export const ALL_BUILT_IN_TOOLS = [
exitTool,
reportFailureTool,
uploadArtifactTool,
eventTool,
];
114 changes: 114 additions & 0 deletions extensions/cli/src/tools/event.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
import { ContinueError, ContinueErrorReason } from "core/util/errors.js";

import {
ApiRequestError,
AuthenticationRequiredError,
} from "../util/apiClient.js";
import { getAgentIdFromArgs, postAgentEvent } from "../util/events.js";
import { logger } from "../util/logger.js";

import { Tool } from "./types.js";

export const eventTool: Tool = {
name: "Event",
displayName: "Event",
description: `Report an activity event for the user to see in the task timeline.

Use this tool to notify the user about significant actions you've taken, such as:
- Creating a pull request
- Posting a comment on a PR or issue
- Pushing commits
- Closing an issue
- Submitting a review

Each event should have:
- eventName: A short identifier for the type of event (e.g., "pr_created", "comment_posted", "commit_pushed")
- title: A human-readable summary of what happened
- description: (optional) Additional details about the event
- externalUrl: (optional) A link to the relevant resource (e.g., GitHub PR URL)

Example usage:
- After creating a PR: eventName="pr_created", title="Created PR #123: Fix authentication bug", externalUrl="https://github.com/org/repo/pull/123"
- After posting a comment: eventName="comment_posted", title="Posted analysis comment on PR #45", externalUrl="https://github.com/org/repo/pull/45#issuecomment-123456"`,
parameters: {
type: "object",
required: ["eventName", "title"],
properties: {
eventName: {
type: "string",
description:
'A short identifier for the event type (e.g., "pr_created", "comment_posted", "commit_pushed", "issue_closed", "review_submitted")',
},
title: {
type: "string",
description:
'A human-readable summary of the event (e.g., "Created PR #123: Fix authentication bug")',
},
description: {
type: "string",
description: "Optional additional details about the event",
},
externalUrl: {
type: "string",
description:
"Optional URL linking to the relevant resource (e.g., GitHub PR or comment URL)",
},
},
},
readonly: true,
isBuiltIn: true,
run: async (args: {
eventName: string;
title: string;
description?: string;
externalUrl?: string;
}): Promise<string> => {
try {
// Get agent ID from --id flag
const agentId = getAgentIdFromArgs();
if (!agentId) {
const errorMessage =
"Agent ID is required. Please use the --id flag with cn serve.";
logger.error(errorMessage);
throw new ContinueError(ContinueErrorReason.Unspecified, errorMessage);
}

// Post the event to the control plane
const result = await postAgentEvent(agentId, {
eventName: args.eventName,
title: args.title,
description: args.description,
externalUrl: args.externalUrl,
});

if (result) {
logger.info(`Event recorded: ${args.eventName} - ${args.title}`);
return `Event recorded: ${args.title}`;
} else {
// Event posting failed but we don't want to fail the tool
logger.warn(`Failed to record event: ${args.eventName}`);
return `Event acknowledged (but may not have been recorded): ${args.title}`;
}
} catch (error) {
if (error instanceof ContinueError) {
throw error;
}

if (error instanceof AuthenticationRequiredError) {
logger.error(error.message);
throw new Error("Error: Authentication required");
}

if (error instanceof ApiRequestError) {
throw new Error(
`Error recording event: ${error.status} ${error.response || error.statusText}`,
);
}

const errorMessage =
error instanceof Error ? error.message : String(error);
logger.error(`Error recording event: ${errorMessage}`);
throw new Error(`Error recording event: ${errorMessage}`);
}
},
};
2 changes: 2 additions & 0 deletions extensions/cli/src/tools/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import { logger } from "../util/logger.js";

import { ALL_BUILT_IN_TOOLS } from "./allBuiltIns.js";
import { editTool } from "./edit.js";
import { eventTool } from "./event.js";
import { exitTool } from "./exit.js";
import { fetchTool } from "./fetch.js";
import { listFilesTool } from "./listFiles.js";
Expand Down Expand Up @@ -82,6 +83,7 @@ export async function getAllAvailableTools(
const agentId = getAgentIdFromArgs();
if (agentId) {
tools.push(reportFailureTool);
tools.push(eventTool);

// UploadArtifact tool is gated behind beta flag
if (isBetaUploadArtifactToolEnabled()) {
Expand Down
111 changes: 111 additions & 0 deletions extensions/cli/src/util/events.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
import {
post,
ApiRequestError,
AuthenticationRequiredError,
} from "./apiClient.js";
import { logger } from "./logger.js";

/**
* Event types that can be emitted to the activity timeline
*/
export type ActionEventName =
| "comment_posted"
| "pr_created"
| "commit_pushed"
| "issue_closed"
| "review_submitted";

/**
* Parameters for emitting an activity event
*/
export interface EmitEventParams {
/** The type of action event */
eventName: ActionEventName | string;
/** Human-readable title for the event */
title: string;
/** Optional longer description */
description?: string;
/** Optional event-specific metadata */
metadata?: Record<string, unknown>;
/** Optional external URL (e.g., link to GitHub PR or comment) */
externalUrl?: string;
}

/**
* Extract the agent ID from the --id command line flag
* @returns The agent ID or undefined if not found
*/
export function getAgentIdFromArgs(): string | undefined {
const args = process.argv;
const idIndex = args.indexOf("--id");
if (idIndex !== -1 && idIndex + 1 < args.length) {
return args[idIndex + 1];
}
return undefined;
}

/**
* POST an activity event to the control plane for an agent session.
* Used to populate the Activity Timeline in the task detail view.
*
* @param agentId - The agent session ID
* @param params - Event parameters
* @returns The created event or undefined on failure
*/
export async function postAgentEvent(
agentId: string,
params: EmitEventParams,
): Promise<Record<string, unknown> | undefined> {
if (!agentId) {
logger.debug("No agent ID provided, skipping event emission");
return undefined;
}

if (!params.eventName || !params.title) {
logger.debug("Missing required event parameters, skipping event emission");
return undefined;
}

try {
logger.debug("Posting event to control plane", {
agentId,
eventName: params.eventName,
title: params.title,
});

const response = await post(`agents/${agentId}/events`, {
eventName: params.eventName,
title: params.title,
description: params.description,
metadata: params.metadata,
externalUrl: params.externalUrl,
});

if (response.ok) {
logger.info("Successfully posted event to control plane", {
eventName: params.eventName,
});
return response.data;
} else {
logger.warn(`Unexpected response when posting event: ${response.status}`);
return undefined;
}
} catch (error) {
// Non-critical: Log but don't fail the entire agent execution
if (error instanceof AuthenticationRequiredError) {
logger.debug(
"Authentication required for event emission (skipping)",
error.message,
);
} else if (error instanceof ApiRequestError) {
logger.warn(
`Failed to post event: ${error.status} ${error.response || error.statusText}`,
);
} else {
const errorMessage =
error instanceof Error ? error.message : String(error);
logger.warn(`Error posting event: ${errorMessage}`);
}
return undefined;
}
}
Loading