Spaces:
Paused
Paused
| import { z } from "zod"; | |
| import { | |
| addIssueCommentSchema, | |
| checkoutIssueSchema, | |
| createApprovalSchema, | |
| createIssueSchema, | |
| updateIssueSchema, | |
| upsertIssueDocumentSchema, | |
| linkIssueApprovalSchema, | |
| } from "@paperclipai/shared"; | |
| import { PaperclipApiClient } from "./client.js"; | |
| import { formatErrorResponse, formatTextResponse } from "./format.js"; | |
| export interface ToolDefinition { | |
| name: string; | |
| description: string; | |
| schema: z.AnyZodObject; | |
| execute: (input: Record<string, unknown>) => Promise<{ | |
| content: Array<{ type: "text"; text: string }>; | |
| }>; | |
| } | |
| function makeTool<TSchema extends z.ZodRawShape>( | |
| name: string, | |
| description: string, | |
| schema: z.ZodObject<TSchema>, | |
| execute: (input: z.infer<typeof schema>) => Promise<unknown>, | |
| ): ToolDefinition { | |
| return { | |
| name, | |
| description, | |
| schema, | |
| execute: async (input) => { | |
| try { | |
| const parsed = schema.parse(input); | |
| return formatTextResponse(await execute(parsed)); | |
| } catch (error) { | |
| return formatErrorResponse(error); | |
| } | |
| }, | |
| }; | |
| } | |
| function parseOptionalJson(raw: string | undefined | null): unknown { | |
| if (!raw || raw.trim().length === 0) return undefined; | |
| return JSON.parse(raw); | |
| } | |
| const companyIdOptional = z.string().uuid().optional().nullable(); | |
| const agentIdOptional = z.string().uuid().optional().nullable(); | |
| const issueIdSchema = z.string().min(1); | |
| const projectIdSchema = z.string().min(1); | |
| const goalIdSchema = z.string().uuid(); | |
| const approvalIdSchema = z.string().uuid(); | |
| const documentKeySchema = z.string().trim().min(1).max(64); | |
| const listIssuesSchema = z.object({ | |
| companyId: companyIdOptional, | |
| status: z.string().optional(), | |
| projectId: z.string().uuid().optional(), | |
| assigneeAgentId: z.string().uuid().optional(), | |
| participantAgentId: z.string().uuid().optional(), | |
| assigneeUserId: z.string().optional(), | |
| touchedByUserId: z.string().optional(), | |
| inboxArchivedByUserId: z.string().optional(), | |
| unreadForUserId: z.string().optional(), | |
| labelId: z.string().uuid().optional(), | |
| executionWorkspaceId: z.string().uuid().optional(), | |
| originKind: z.string().optional(), | |
| originId: z.string().optional(), | |
| includeRoutineExecutions: z.boolean().optional(), | |
| q: z.string().optional(), | |
| }); | |
| const listCommentsSchema = z.object({ | |
| issueId: issueIdSchema, | |
| after: z.string().uuid().optional(), | |
| order: z.enum(["asc", "desc"]).optional(), | |
| limit: z.number().int().positive().max(500).optional(), | |
| }); | |
| const upsertDocumentToolSchema = z.object({ | |
| issueId: issueIdSchema, | |
| key: documentKeySchema, | |
| title: z.string().trim().max(200).nullable().optional(), | |
| format: z.enum(["markdown"]).default("markdown"), | |
| body: z.string().max(524288), | |
| changeSummary: z.string().trim().max(500).nullable().optional(), | |
| baseRevisionId: z.string().uuid().nullable().optional(), | |
| }); | |
| const createIssueToolSchema = z.object({ | |
| companyId: companyIdOptional, | |
| }).merge(createIssueSchema); | |
| const updateIssueToolSchema = z.object({ | |
| issueId: issueIdSchema, | |
| }).merge(updateIssueSchema); | |
| const checkoutIssueToolSchema = z.object({ | |
| issueId: issueIdSchema, | |
| agentId: agentIdOptional, | |
| expectedStatuses: checkoutIssueSchema.shape.expectedStatuses.optional(), | |
| }); | |
| const addCommentToolSchema = z.object({ | |
| issueId: issueIdSchema, | |
| }).merge(addIssueCommentSchema); | |
| const approvalDecisionSchema = z.object({ | |
| approvalId: approvalIdSchema, | |
| action: z.enum(["approve", "reject", "requestRevision", "resubmit"]), | |
| decisionNote: z.string().optional(), | |
| payloadJson: z.string().optional(), | |
| }); | |
| const createApprovalToolSchema = z.object({ | |
| companyId: companyIdOptional, | |
| }).merge(createApprovalSchema); | |
| const apiRequestSchema = z.object({ | |
| method: z.enum(["GET", "POST", "PUT", "PATCH", "DELETE"]), | |
| path: z.string().min(1), | |
| jsonBody: z.string().optional(), | |
| }); | |
| export function createToolDefinitions(client: PaperclipApiClient): ToolDefinition[] { | |
| return [ | |
| makeTool( | |
| "paperclipMe", | |
| "Get the current authenticated Paperclip actor details", | |
| z.object({}), | |
| async () => client.requestJson("GET", "/agents/me"), | |
| ), | |
| makeTool( | |
| "paperclipInboxLite", | |
| "Get the current authenticated agent inbox-lite assignment list", | |
| z.object({}), | |
| async () => client.requestJson("GET", "/agents/me/inbox-lite"), | |
| ), | |
| makeTool( | |
| "paperclipListAgents", | |
| "List agents in a company", | |
| z.object({ companyId: companyIdOptional }), | |
| async ({ companyId }) => client.requestJson("GET", `/companies/${client.resolveCompanyId(companyId)}/agents`), | |
| ), | |
| makeTool( | |
| "paperclipGetAgent", | |
| "Get a single agent by id", | |
| z.object({ agentId: z.string().min(1), companyId: companyIdOptional }), | |
| async ({ agentId, companyId }) => { | |
| const qs = companyId ? `?companyId=${encodeURIComponent(companyId)}` : ""; | |
| return client.requestJson("GET", `/agents/${encodeURIComponent(agentId)}${qs}`); | |
| }, | |
| ), | |
| makeTool( | |
| "paperclipListIssues", | |
| "List issues for a company with optional filters", | |
| listIssuesSchema, | |
| async (input) => { | |
| const companyId = client.resolveCompanyId(input.companyId); | |
| const params = new URLSearchParams(); | |
| for (const [key, value] of Object.entries(input)) { | |
| if (key === "companyId" || value === undefined || value === null) continue; | |
| params.set(key, String(value)); | |
| } | |
| const qs = params.toString(); | |
| return client.requestJson("GET", `/companies/${companyId}/issues${qs ? `?${qs}` : ""}`); | |
| }, | |
| ), | |
| makeTool( | |
| "paperclipGetIssue", | |
| "Get a single issue by UUID or identifier", | |
| z.object({ issueId: issueIdSchema }), | |
| async ({ issueId }) => client.requestJson("GET", `/issues/${encodeURIComponent(issueId)}`), | |
| ), | |
| makeTool( | |
| "paperclipGetHeartbeatContext", | |
| "Get compact heartbeat context for an issue", | |
| z.object({ issueId: issueIdSchema, wakeCommentId: z.string().uuid().optional() }), | |
| async ({ issueId, wakeCommentId }) => { | |
| const qs = wakeCommentId ? `?wakeCommentId=${encodeURIComponent(wakeCommentId)}` : ""; | |
| return client.requestJson("GET", `/issues/${encodeURIComponent(issueId)}/heartbeat-context${qs}`); | |
| }, | |
| ), | |
| makeTool( | |
| "paperclipListComments", | |
| "List issue comments with incremental options", | |
| listCommentsSchema, | |
| async ({ issueId, after, order, limit }) => { | |
| const params = new URLSearchParams(); | |
| if (after) params.set("after", after); | |
| if (order) params.set("order", order); | |
| if (limit) params.set("limit", String(limit)); | |
| const qs = params.toString(); | |
| return client.requestJson("GET", `/issues/${encodeURIComponent(issueId)}/comments${qs ? `?${qs}` : ""}`); | |
| }, | |
| ), | |
| makeTool( | |
| "paperclipGetComment", | |
| "Get a specific issue comment by id", | |
| z.object({ issueId: issueIdSchema, commentId: z.string().uuid() }), | |
| async ({ issueId, commentId }) => | |
| client.requestJson("GET", `/issues/${encodeURIComponent(issueId)}/comments/${encodeURIComponent(commentId)}`), | |
| ), | |
| makeTool( | |
| "paperclipListIssueApprovals", | |
| "List approvals linked to an issue", | |
| z.object({ issueId: issueIdSchema }), | |
| async ({ issueId }) => client.requestJson("GET", `/issues/${encodeURIComponent(issueId)}/approvals`), | |
| ), | |
| makeTool( | |
| "paperclipListDocuments", | |
| "List issue documents", | |
| z.object({ issueId: issueIdSchema }), | |
| async ({ issueId }) => client.requestJson("GET", `/issues/${encodeURIComponent(issueId)}/documents`), | |
| ), | |
| makeTool( | |
| "paperclipGetDocument", | |
| "Get one issue document by key", | |
| z.object({ issueId: issueIdSchema, key: documentKeySchema }), | |
| async ({ issueId, key }) => | |
| client.requestJson("GET", `/issues/${encodeURIComponent(issueId)}/documents/${encodeURIComponent(key)}`), | |
| ), | |
| makeTool( | |
| "paperclipListDocumentRevisions", | |
| "List revisions for an issue document", | |
| z.object({ issueId: issueIdSchema, key: documentKeySchema }), | |
| async ({ issueId, key }) => | |
| client.requestJson( | |
| "GET", | |
| `/issues/${encodeURIComponent(issueId)}/documents/${encodeURIComponent(key)}/revisions`, | |
| ), | |
| ), | |
| makeTool( | |
| "paperclipListProjects", | |
| "List projects in a company", | |
| z.object({ companyId: companyIdOptional }), | |
| async ({ companyId }) => client.requestJson("GET", `/companies/${client.resolveCompanyId(companyId)}/projects`), | |
| ), | |
| makeTool( | |
| "paperclipGetProject", | |
| "Get a project by id or company-scoped short reference", | |
| z.object({ projectId: projectIdSchema, companyId: companyIdOptional }), | |
| async ({ projectId, companyId }) => { | |
| const qs = companyId ? `?companyId=${encodeURIComponent(companyId)}` : ""; | |
| return client.requestJson("GET", `/projects/${encodeURIComponent(projectId)}${qs}`); | |
| }, | |
| ), | |
| makeTool( | |
| "paperclipListGoals", | |
| "List goals in a company", | |
| z.object({ companyId: companyIdOptional }), | |
| async ({ companyId }) => client.requestJson("GET", `/companies/${client.resolveCompanyId(companyId)}/goals`), | |
| ), | |
| makeTool( | |
| "paperclipGetGoal", | |
| "Get a goal by id", | |
| z.object({ goalId: goalIdSchema }), | |
| async ({ goalId }) => client.requestJson("GET", `/goals/${encodeURIComponent(goalId)}`), | |
| ), | |
| makeTool( | |
| "paperclipListApprovals", | |
| "List approvals in a company", | |
| z.object({ companyId: companyIdOptional, status: z.string().optional() }), | |
| async ({ companyId, status }) => { | |
| const qs = status ? `?status=${encodeURIComponent(status)}` : ""; | |
| return client.requestJson("GET", `/companies/${client.resolveCompanyId(companyId)}/approvals${qs}`); | |
| }, | |
| ), | |
| makeTool( | |
| "paperclipCreateApproval", | |
| "Create a board approval request, optionally linked to one or more issues", | |
| createApprovalToolSchema, | |
| async ({ companyId, ...body }) => | |
| client.requestJson("POST", `/companies/${client.resolveCompanyId(companyId)}/approvals`, { | |
| body, | |
| }), | |
| ), | |
| makeTool( | |
| "paperclipGetApproval", | |
| "Get an approval by id", | |
| z.object({ approvalId: approvalIdSchema }), | |
| async ({ approvalId }) => client.requestJson("GET", `/approvals/${encodeURIComponent(approvalId)}`), | |
| ), | |
| makeTool( | |
| "paperclipGetApprovalIssues", | |
| "List issues linked to an approval", | |
| z.object({ approvalId: approvalIdSchema }), | |
| async ({ approvalId }) => client.requestJson("GET", `/approvals/${encodeURIComponent(approvalId)}/issues`), | |
| ), | |
| makeTool( | |
| "paperclipListApprovalComments", | |
| "List comments for an approval", | |
| z.object({ approvalId: approvalIdSchema }), | |
| async ({ approvalId }) => client.requestJson("GET", `/approvals/${encodeURIComponent(approvalId)}/comments`), | |
| ), | |
| makeTool( | |
| "paperclipCreateIssue", | |
| "Create a new issue", | |
| createIssueToolSchema, | |
| async ({ companyId, ...body }) => | |
| client.requestJson("POST", `/companies/${client.resolveCompanyId(companyId)}/issues`, { body }), | |
| ), | |
| makeTool( | |
| "paperclipUpdateIssue", | |
| "Patch an issue, optionally including a comment", | |
| updateIssueToolSchema, | |
| async ({ issueId, ...body }) => | |
| client.requestJson("PATCH", `/issues/${encodeURIComponent(issueId)}`, { body }), | |
| ), | |
| makeTool( | |
| "paperclipCheckoutIssue", | |
| "Checkout an issue for an agent", | |
| checkoutIssueToolSchema, | |
| async ({ issueId, agentId, expectedStatuses }) => | |
| client.requestJson("POST", `/issues/${encodeURIComponent(issueId)}/checkout`, { | |
| body: { | |
| agentId: client.resolveAgentId(agentId), | |
| expectedStatuses: expectedStatuses ?? ["todo", "backlog", "blocked"], | |
| }, | |
| }), | |
| ), | |
| makeTool( | |
| "paperclipReleaseIssue", | |
| "Release an issue checkout", | |
| z.object({ issueId: issueIdSchema }), | |
| async ({ issueId }) => client.requestJson("POST", `/issues/${encodeURIComponent(issueId)}/release`, { body: {} }), | |
| ), | |
| makeTool( | |
| "paperclipAddComment", | |
| "Add a comment to an issue", | |
| addCommentToolSchema, | |
| async ({ issueId, ...body }) => | |
| client.requestJson("POST", `/issues/${encodeURIComponent(issueId)}/comments`, { body }), | |
| ), | |
| makeTool( | |
| "paperclipUpsertIssueDocument", | |
| "Create or update an issue document", | |
| upsertDocumentToolSchema, | |
| async ({ issueId, key, ...body }) => | |
| client.requestJson( | |
| "PUT", | |
| `/issues/${encodeURIComponent(issueId)}/documents/${encodeURIComponent(key)}`, | |
| { body }, | |
| ), | |
| ), | |
| makeTool( | |
| "paperclipRestoreIssueDocumentRevision", | |
| "Restore a prior revision of an issue document", | |
| z.object({ | |
| issueId: issueIdSchema, | |
| key: documentKeySchema, | |
| revisionId: z.string().uuid(), | |
| }), | |
| async ({ issueId, key, revisionId }) => | |
| client.requestJson( | |
| "POST", | |
| `/issues/${encodeURIComponent(issueId)}/documents/${encodeURIComponent(key)}/revisions/${encodeURIComponent(revisionId)}/restore`, | |
| { body: {} }, | |
| ), | |
| ), | |
| makeTool( | |
| "paperclipLinkIssueApproval", | |
| "Link an approval to an issue", | |
| z.object({ issueId: issueIdSchema }).merge(linkIssueApprovalSchema), | |
| async ({ issueId, approvalId }) => | |
| client.requestJson("POST", `/issues/${encodeURIComponent(issueId)}/approvals`, { | |
| body: { approvalId }, | |
| }), | |
| ), | |
| makeTool( | |
| "paperclipUnlinkIssueApproval", | |
| "Unlink an approval from an issue", | |
| z.object({ issueId: issueIdSchema, approvalId: approvalIdSchema }), | |
| async ({ issueId, approvalId }) => | |
| client.requestJson( | |
| "DELETE", | |
| `/issues/${encodeURIComponent(issueId)}/approvals/${encodeURIComponent(approvalId)}`, | |
| ), | |
| ), | |
| makeTool( | |
| "paperclipApprovalDecision", | |
| "Approve, reject, request revision, or resubmit an approval", | |
| approvalDecisionSchema, | |
| async ({ approvalId, action, decisionNote, payloadJson }) => { | |
| const path = | |
| action === "approve" | |
| ? `/approvals/${encodeURIComponent(approvalId)}/approve` | |
| : action === "reject" | |
| ? `/approvals/${encodeURIComponent(approvalId)}/reject` | |
| : action === "requestRevision" | |
| ? `/approvals/${encodeURIComponent(approvalId)}/request-revision` | |
| : `/approvals/${encodeURIComponent(approvalId)}/resubmit`; | |
| const body = | |
| action === "resubmit" | |
| ? { payload: parseOptionalJson(payloadJson) ?? {} } | |
| : { decisionNote }; | |
| return client.requestJson("POST", path, { body }); | |
| }, | |
| ), | |
| makeTool( | |
| "paperclipAddApprovalComment", | |
| "Add a comment to an approval", | |
| z.object({ approvalId: approvalIdSchema, body: z.string().min(1) }), | |
| async ({ approvalId, body }) => | |
| client.requestJson("POST", `/approvals/${encodeURIComponent(approvalId)}/comments`, { | |
| body: { body }, | |
| }), | |
| ), | |
| makeTool( | |
| "paperclipApiRequest", | |
| "Make a JSON request to an existing Paperclip /api endpoint for unsupported operations", | |
| apiRequestSchema, | |
| async ({ method, path, jsonBody }) => { | |
| if (!path.startsWith("/") || path.includes("..")) { | |
| throw new Error("path must start with / and be relative to /api, and must not contain '..'"); | |
| } | |
| return client.requestJson(method, path, { | |
| body: parseOptionalJson(jsonBody), | |
| }); | |
| }, | |
| ), | |
| ]; | |
| } | |