import { z } from "zod"; import { repoDb } from "../../scoped-db"; import { accountOrGhost, accountsById } from "../../people"; import { notFoundError } from "../../errors"; import { issueUrl, loadIssue } from "../../issues"; import type { IssueRow } from "../../issues"; import { limitWrite } from "../../limits"; import { markdownBody, title } from "~/lib/validation"; import { changeIssueState, commentsWithAuthors, insertComment, insertIssue } from "../issue-writes"; import { attributionNote, defineTool, repoArgs, scopedRepo, truncateText } from "./base"; const LIST_LIMIT = 50; const MAX_COMMENTS = 100; const number = z.number().int().positive(); const listIssuesTool = defineTool({ name: "list_issues", title: "List issues", description: "Issues (not pull requests) in a repository, newest first.", input: z.object({ ...repoArgs, state: z.enum(["open", "closed", "all"]).default("open"), }), write: false, async run({ caller }, args) { const { repo } = await scopedRepo(caller, args.owner, args.repo, "issues", "read"); const issues = await repoDb(repo.id).all( `SELECT i.* FROM issues i WHERE i.repo_id = ? AND i.is_pull = 0 AND i.hidden_at IS NULL AND (? = 'all' OR i.state = ?) ORDER BY i.number DESC LIMIT ${LIST_LIMIT}`, repo.id, args.state, args.state, ); const authors = await accountsById(issues.map((row) => row.author_id)); const rows = issues.map((row) => ({ ...row, author: accountOrGhost(authors, row.author_id).name, })); return { structured: { issues: rows.map((row) => ({ number: row.number, title: row.title, state: row.state, author: row.author, comments: row.comments_count, html_url: issueUrl(repo, row), })), }, repoId: repo.id, }; }, }); const getIssueTool = defineTool({ name: "get_issue", title: "Get issue", description: "An issue or pull request conversation: body and up to 100 comments.", input: z.object({ ...repoArgs, number }), write: false, async run({ caller }, args) { const { repo } = await scopedRepo(caller, args.owner, args.repo, "issues", "read"); const issue = await loadIssue(repo.id, args.number); if (!issue) throw notFoundError("Issue not found"); if (issue.is_pull === 1) await scopedRepo(caller, args.owner, args.repo, "pulls", "read"); const comments = await commentsWithAuthors(repo.id, issue.id, MAX_COMMENTS); return { structured: { number: issue.number, title: issue.title, body: truncateText(issue.body).text, state: issue.state, is_pull: issue.is_pull === 1, locked: issue.locked === 1, html_url: issueUrl(repo, issue), comments: comments.map((comment) => ({ ...comment, body: truncateText(comment.body, 8000).text, })), comments_truncated: issue.comments_count > MAX_COMMENTS, }, repoId: repo.id, }; }, }); const createIssueTool = defineTool({ name: "create_issue", title: "Create issue", description: "Open an issue. The body records which agent filed it and for whom.", input: z.object({ ...repoArgs, title, body: markdownBody.default("") }), write: true, async run({ caller }, args) { await limitWrite(caller.viewer); const { repo } = await scopedRepo(caller, args.owner, args.repo, "issues", "write"); const footer = `\n\n---\n${attributionNote(caller, "Filed")}`; const issueNumber = await insertIssue(repo, caller.viewer, args.title, `${args.body}${footer}`); return { structured: { number: issueNumber, html_url: issueUrl(repo, { number: issueNumber, is_pull: 0 }), }, repoId: repo.id, }; }, }); const commentTool = defineTool({ name: "comment", title: "Comment on issue or pull request", description: "Add a comment to an issue or pull request conversation.", input: z.object({ ...repoArgs, number, body: markdownBody.min(1) }), write: true, async run({ caller }, args) { await limitWrite(caller.viewer); const { repo } = await scopedRepo(caller, args.owner, args.repo, "issues", "read"); const issue = await loadIssue(repo.id, args.number); if (!issue) throw notFoundError("Issue not found"); await scopedRepo(caller, args.owner, args.repo, issue.is_pull === 1 ? "pulls" : "issues", "write"); const id = await insertComment(repo, issue, caller.viewer, args.body); return { structured: { id, html_url: issueUrl(repo, issue) }, repoId: repo.id, }; }, }); const setStateTool = defineTool({ name: "set_issue_state", title: "Close or reopen", description: "Close or reopen an issue or pull request.", input: z.object({ ...repoArgs, number, state: z.enum(["open", "closed"]) }), write: true, async run({ caller }, args) { await limitWrite(caller.viewer); const { repo } = await scopedRepo(caller, args.owner, args.repo, "issues", "read"); const issue = await loadIssue(repo.id, args.number); if (!issue) throw notFoundError("Issue not found"); await scopedRepo(caller, args.owner, args.repo, issue.is_pull === 1 ? "pulls" : "issues", "write"); await changeIssueState(repo, issue, caller.viewer, args.state); return { structured: { number: issue.number, state: args.state }, repoId: repo.id, }; }, }); export const issueTools = [listIssuesTool, getIssueTool, createIssueTool, commentTool, setStateTool];