Tool factories
This commit is contained in:
+102
-104
@@ -2,10 +2,11 @@ import { Octokit } from "@octokit/rest";
|
||||
import { type } from "arktype";
|
||||
import type { Payload } from "../external.ts";
|
||||
import { agentsManifest } from "../external.ts";
|
||||
import type { Context } from "../main.ts";
|
||||
import { fetchWorkflowRunInfo } from "../utils/api.ts";
|
||||
import { buildPullfrogFooter, stripExistingFooter } from "../utils/buildPullfrogFooter.ts";
|
||||
import { getGitHubInstallationToken, parseRepoContext } from "../utils/github.ts";
|
||||
import { contextualize, getMcpContext, tool } from "./shared.ts";
|
||||
import { execute, tool } from "./shared.ts";
|
||||
|
||||
/**
|
||||
* The prefix text for the initial "leaping into action" comment.
|
||||
@@ -42,58 +43,62 @@ export const Comment = type({
|
||||
body: type.string.describe("the comment body content"),
|
||||
});
|
||||
|
||||
export const CreateCommentTool = tool({
|
||||
name: "create_issue_comment",
|
||||
description:
|
||||
"Create a comment on a GitHub issue. NOTE: Do NOT use this for progress updates or status summaries - use report_progress instead, which updates the existing progress comment.",
|
||||
parameters: Comment,
|
||||
execute: contextualize(async ({ issueNumber, body }, ctx) => {
|
||||
const bodyWithFooter = addFooter(body, ctx.payload);
|
||||
export function CreateCommentTool(ctx: Context) {
|
||||
return tool({
|
||||
name: "create_issue_comment",
|
||||
description:
|
||||
"Create a comment on a GitHub issue. NOTE: Do NOT use this for progress updates or status summaries - use report_progress instead, which updates the existing progress comment.",
|
||||
parameters: Comment,
|
||||
execute: execute(ctx, async ({ issueNumber, body }) => {
|
||||
const bodyWithFooter = addFooter(body, ctx.payload);
|
||||
|
||||
const result = await ctx.octokit.rest.issues.createComment({
|
||||
owner: ctx.owner,
|
||||
repo: ctx.name,
|
||||
issue_number: issueNumber,
|
||||
body: bodyWithFooter,
|
||||
});
|
||||
const result = await ctx.octokit.rest.issues.createComment({
|
||||
owner: ctx.owner,
|
||||
repo: ctx.name,
|
||||
issue_number: issueNumber,
|
||||
body: bodyWithFooter,
|
||||
});
|
||||
|
||||
return {
|
||||
success: true,
|
||||
commentId: result.data.id,
|
||||
url: result.data.html_url,
|
||||
body: result.data.body,
|
||||
};
|
||||
}),
|
||||
});
|
||||
return {
|
||||
success: true,
|
||||
commentId: result.data.id,
|
||||
url: result.data.html_url,
|
||||
body: result.data.body,
|
||||
};
|
||||
}),
|
||||
});
|
||||
}
|
||||
|
||||
export const EditComment = type({
|
||||
commentId: type.number.describe("the ID of the comment to edit"),
|
||||
body: type.string.describe("the new comment body content"),
|
||||
});
|
||||
|
||||
export const EditCommentTool = tool({
|
||||
name: "edit_issue_comment",
|
||||
description: "Edit a GitHub issue comment by its ID",
|
||||
parameters: EditComment,
|
||||
execute: contextualize(async ({ commentId, body }, ctx) => {
|
||||
const bodyWithFooter = addFooter(body, ctx.payload);
|
||||
export function EditCommentTool(ctx: Context) {
|
||||
return tool({
|
||||
name: "edit_issue_comment",
|
||||
description: "Edit a GitHub issue comment by its ID",
|
||||
parameters: EditComment,
|
||||
execute: execute(ctx, async ({ commentId, body }) => {
|
||||
const bodyWithFooter = addFooter(body, ctx.payload);
|
||||
|
||||
const result = await ctx.octokit.rest.issues.updateComment({
|
||||
owner: ctx.owner,
|
||||
repo: ctx.name,
|
||||
comment_id: commentId,
|
||||
body: bodyWithFooter,
|
||||
});
|
||||
const result = await ctx.octokit.rest.issues.updateComment({
|
||||
owner: ctx.owner,
|
||||
repo: ctx.name,
|
||||
comment_id: commentId,
|
||||
body: bodyWithFooter,
|
||||
});
|
||||
|
||||
return {
|
||||
success: true,
|
||||
commentId: result.data.id,
|
||||
url: result.data.html_url,
|
||||
body: result.data.body,
|
||||
updatedAt: result.data.updated_at,
|
||||
};
|
||||
}),
|
||||
});
|
||||
return {
|
||||
success: true,
|
||||
commentId: result.data.id,
|
||||
url: result.data.html_url,
|
||||
body: result.data.body,
|
||||
updatedAt: result.data.updated_at,
|
||||
};
|
||||
}),
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Get progress comment ID from environment variable.
|
||||
@@ -141,7 +146,10 @@ export const ReportProgress = type({
|
||||
* Can be called directly without going through the MCP tool interface.
|
||||
* Returns result data if successful, undefined if comment cannot be created.
|
||||
*/
|
||||
export async function reportProgress({ body }: { body: string }): Promise<
|
||||
export async function reportProgress(
|
||||
ctx: Context,
|
||||
{ body }: { body: string }
|
||||
): Promise<
|
||||
| {
|
||||
commentId: number;
|
||||
url: string;
|
||||
@@ -150,8 +158,6 @@ export async function reportProgress({ body }: { body: string }): Promise<
|
||||
}
|
||||
| undefined
|
||||
> {
|
||||
const ctx = await getMcpContext();
|
||||
|
||||
const bodyWithFooter = addFooter(body, ctx.payload);
|
||||
const existingCommentId = getProgressCommentId();
|
||||
|
||||
@@ -200,30 +206,32 @@ export async function reportProgress({ body }: { body: string }): Promise<
|
||||
};
|
||||
}
|
||||
|
||||
export const ReportProgressTool = tool({
|
||||
name: "report_progress",
|
||||
description:
|
||||
"Share progress on the associated GitHub issue/PR. Call this to post updates as you work. The first call creates a comment, subsequent calls update it. Use this throughout your work to keep stakeholders informed.",
|
||||
parameters: ReportProgress,
|
||||
execute: contextualize(async ({ body }) => {
|
||||
const result = await reportProgress({ body });
|
||||
export function ReportProgressTool(ctx: Context) {
|
||||
return tool({
|
||||
name: "report_progress",
|
||||
description:
|
||||
"Share progress on the associated GitHub issue/PR. Call this to post updates as you work. The first call creates a comment, subsequent calls update it. Use this throughout your work to keep stakeholders informed.",
|
||||
parameters: ReportProgress,
|
||||
execute: execute(ctx, async ({ body }) => {
|
||||
const result = await reportProgress(ctx, { body });
|
||||
|
||||
if (!result) {
|
||||
// gracefully handle case where no comment can be created
|
||||
// this happens for workflow_dispatch events or when there's no associated issue/PR
|
||||
return {
|
||||
success: false,
|
||||
message:
|
||||
"cannot create progress comment: no issue_number found in the payload event. this may occur for workflow_dispatch events or when there is no associated issue/PR. if you need to comment on a specific issue or PR, use create_issue_comment with an explicit issueNumber.",
|
||||
};
|
||||
}
|
||||
|
||||
if (!result) {
|
||||
// gracefully handle case where no comment can be created
|
||||
// this happens for workflow_dispatch events or when there's no associated issue/PR
|
||||
return {
|
||||
success: false,
|
||||
message:
|
||||
"cannot create progress comment: no issue_number found in the payload event. this may occur for workflow_dispatch events or when there is no associated issue/PR. if you need to comment on a specific issue or PR, use create_issue_comment with an explicit issueNumber.",
|
||||
success: true,
|
||||
...result,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
...result,
|
||||
};
|
||||
}),
|
||||
});
|
||||
}),
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the progress comment was updated during execution
|
||||
@@ -236,14 +244,12 @@ export function wasProgressCommentUpdated(): boolean {
|
||||
* Delete the progress comment if it exists.
|
||||
* Used after submitting a PR review since the review body contains all necessary info.
|
||||
*/
|
||||
export async function deleteProgressComment(): Promise<boolean> {
|
||||
export async function deleteProgressComment(ctx: Context): Promise<boolean> {
|
||||
const existingCommentId = getProgressCommentId();
|
||||
if (!existingCommentId) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const ctx = await getMcpContext();
|
||||
|
||||
await ctx.octokit.rest.issues.deleteComment({
|
||||
owner: ctx.owner,
|
||||
repo: ctx.name,
|
||||
@@ -321,16 +327,6 @@ export async function ensureProgressCommentUpdated(payload?: Payload): Promise<v
|
||||
return;
|
||||
}
|
||||
|
||||
// try to get payload from MCP context if available, otherwise use provided payload
|
||||
let resolvedPayload: Payload | undefined;
|
||||
try {
|
||||
const ctx = await getMcpContext();
|
||||
resolvedPayload = ctx.payload;
|
||||
} catch {
|
||||
// MCP context not initialized, use provided payload
|
||||
resolvedPayload = payload;
|
||||
}
|
||||
|
||||
const runId = process.env.GITHUB_RUN_ID;
|
||||
const workflowRunLink = runId
|
||||
? `[workflow](https://github.com/${repoContext.owner}/${repoContext.name}/actions/runs/${runId})`
|
||||
@@ -341,7 +337,7 @@ export async function ensureProgressCommentUpdated(payload?: Payload): Promise<v
|
||||
The workflow encountered an error before any progress could be reported. Please check the ${workflowRunLink} for details.`;
|
||||
|
||||
// add footer if we have payload, otherwise use plain message
|
||||
const body = resolvedPayload ? addFooter(errorMessage, resolvedPayload) : errorMessage;
|
||||
const body = payload ? addFooter(errorMessage, payload) : errorMessage;
|
||||
|
||||
await octokit.rest.issues.updateComment({
|
||||
owner: repoContext.owner,
|
||||
@@ -359,28 +355,30 @@ export const ReplyToReviewComment = type({
|
||||
),
|
||||
});
|
||||
|
||||
export const ReplyToReviewCommentTool = tool({
|
||||
name: "reply_to_review_comment",
|
||||
description:
|
||||
"Reply to a PR review comment thread. Call this for EACH comment you address. Keep replies extremely brief (1 sentence max).",
|
||||
parameters: ReplyToReviewComment,
|
||||
execute: contextualize(async ({ pull_number, comment_id, body }, ctx) => {
|
||||
const bodyWithFooter = addFooter(body, ctx.payload);
|
||||
export function ReplyToReviewCommentTool(ctx: Context) {
|
||||
return tool({
|
||||
name: "reply_to_review_comment",
|
||||
description:
|
||||
"Reply to a PR review comment thread. Call this for EACH comment you address. Keep replies extremely brief (1 sentence max).",
|
||||
parameters: ReplyToReviewComment,
|
||||
execute: execute(ctx, async ({ pull_number, comment_id, body }) => {
|
||||
const bodyWithFooter = addFooter(body, ctx.payload);
|
||||
|
||||
const result = await ctx.octokit.rest.pulls.createReplyForReviewComment({
|
||||
owner: ctx.owner,
|
||||
repo: ctx.name,
|
||||
pull_number,
|
||||
comment_id,
|
||||
body: bodyWithFooter,
|
||||
});
|
||||
const result = await ctx.octokit.rest.pulls.createReplyForReviewComment({
|
||||
owner: ctx.owner,
|
||||
repo: ctx.name,
|
||||
pull_number,
|
||||
comment_id,
|
||||
body: bodyWithFooter,
|
||||
});
|
||||
|
||||
return {
|
||||
success: true,
|
||||
commentId: result.data.id,
|
||||
url: result.data.html_url,
|
||||
body: result.data.body,
|
||||
in_reply_to_id: result.data.in_reply_to_id,
|
||||
};
|
||||
}),
|
||||
});
|
||||
return {
|
||||
success: true,
|
||||
commentId: result.data.id,
|
||||
url: result.data.html_url,
|
||||
body: result.data.body,
|
||||
in_reply_to_id: result.data.in_reply_to_id,
|
||||
};
|
||||
}),
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user