Bind Paperclip Runner read tools

This commit is contained in:
Dotta committed 2026-08-21 08:32:17 -05:00
1 parent 83d58b860b
commit 00b9012a28
2 files changed
+101 -6

No files matched your search

@@ -53,7 +53,12 @@ describe("PaperclipRunnerToolAuthority", () => {
it("advertises only real bindings and reads the bound task", async () => {
const authority = new PaperclipRunnerToolAuthority(db, { companyId, agentId, issueId, runId });
expect(authority.definitions().map((tool) => tool.name)).toEqual(["get_task_context", "report_progress"]);
expect(authority.definitions()).toHaveLength(12);
expect(authority.definitions().map((tool) => tool.name)).toEqual(expect.arrayContaining([
"get_task_context", "get_task_history", "search_tasks", "report_progress",
"list_documents", "read_document", "list_document_revisions",
"list_agents", "get_agent", "list_approvals", "get_approval", "get_approval_context",
]));
await expect(authority.execute({ tool: "get_task_context", callId: "context", arguments: {} }))
.resolves.toMatchObject({ activeTask: { id: issueId, identifier: "RNT-1" }, actor: { id: agentId } });
await expect(authority.execute({ tool: "finish_task", callId: "hidden", arguments: {} }))
@@ -1,10 +1,17 @@
import { and, eq } from "drizzle-orm";
import type { Db } from "@paperclipai/db";
import { agents, heartbeatRuns, issues } from "@paperclipai/db";
import { agents, heartbeatRuns, issueApprovals, issueComments, issues } from "@paperclipai/db";
import { CAPABILITY_SEMANTIC_TOOL_CATALOG } from "../../vendor/paperclip-runner/index.js";
import { agentService } from "../agents.js";
import { approvalService } from "../approvals.js";
import { documentService } from "../documents.js";
import { issueService } from "../issues.js";
const IMPLEMENTED_OPERATIONS = new Set(["get_task_context", "report_progress"]);
const IMPLEMENTED_OPERATIONS = new Set([
"get_task_context", "get_task_history", "search_tasks", "report_progress",
"list_documents", "read_document", "list_document_revisions",
"list_agents", "get_agent", "list_approvals", "get_approval", "get_approval_context",
]);
type Binding = {
companyId: string;
@@ -50,15 +57,67 @@ export class PaperclipRunnerToolAuthority {
async execute(call: { tool: string; callId: string; arguments: unknown }): Promise<unknown> {
if (!IMPLEMENTED_OPERATIONS.has(call.tool)) throw new Error("paperclip_runner_tool_not_advertised");
const context = await this.#boundContext();
if (call.tool === "get_task_context") {
return {
const input = record(call.arguments);
switch (call.tool) {
case "get_task_context": return {
company: { id: this.binding.companyId },
actor: context.actor,
activeTask: context.issue,
run: { id: this.binding.runId },
};
case "get_task_history": {
const limit = boundedLimit(input.limit);
const comments = await this.db.select().from(issueComments)
.where(eq(issueComments.issueId, this.binding.issueId));
return { comments: comments.slice(-limit) };
}
case "search_tasks": {
const tasks = await issueService(this.db).list(this.binding.companyId);
const query = typeof input.query === "string" ? input.query.toLowerCase() : "";
const statuses = Array.isArray(input.statuses) ? new Set(input.statuses.filter((value): value is string => typeof value === "string")) : null;
return { tasks: tasks.filter((task) =>
(!query || `${task.identifier} ${task.title} ${task.description ?? ""}`.toLowerCase().includes(query))
&& (!statuses || statuses.size === 0 || statuses.has(task.status))
).slice(0, boundedLimit(input.limit)) };
}
case "list_documents":
return { documents: await documentService(this.db).listIssueDocuments(this.binding.issueId) };
case "read_document": {
const document = await documentService(this.db).getIssueDocumentByKey(this.binding.issueId, requiredString(input.key));
if (!document) throw new Error("paperclip_runner_document_not_found");
return { document };
}
case "list_document_revisions":
return { revisions: await documentService(this.db).listIssueDocumentRevisions(this.binding.issueId, requiredString(input.key)) };
case "list_agents":
return { actors: (await agentService(this.db).list(this.binding.companyId)).map(redactedActor) };
case "get_agent": {
const actor = await agentService(this.db).getById(requiredString(input.actorId));
if (!actor || actor.companyId !== this.binding.companyId) throw new Error("paperclip_runner_agent_not_found");
return { actor: redactedActor(actor) };
}
case "list_approvals":
return { approvals: await approvalService(this.db).list(this.binding.companyId) };
case "get_approval": {
const approval = await this.#approval(requiredString(input.approvalId));
return { approval };
}
case "get_approval_context": {
const approval = await this.#approval(requiredString(input.approvalId));
const tasks = await this.db.select({ issue: issues }).from(issueApprovals)
.innerJoin(issues, eq(issues.id, issueApprovals.issueId))
.where(eq(issueApprovals.approvalId, approval.id));
return { approval, tasks: tasks.map((row) => row.issue) };
}
case "report_progress": return this.#reportProgress(call.callId, call.arguments);
default: throw new Error("paperclip_runner_tool_not_bound");
}
return this.#reportProgress(call.callId, call.arguments);
}
async #approval(id: string) {
const approval = await approvalService(this.db).getById(id);
if (!approval || approval.companyId !== this.binding.companyId) throw new Error("paperclip_runner_approval_not_found");
return approval;
}
async #boundContext() {
@@ -120,3 +179,34 @@ export class PaperclipRunnerToolAuthority {
});
}
}
function requiredString(value: unknown): string {
if (typeof value !== "string" || value.trim() === "") throw new Error("paperclip_runner_tool_input_invalid");
return value.trim();
}
function boundedLimit(value: unknown): number {
return typeof value === "number" && Number.isInteger(value)
? Math.max(1, Math.min(value, 100))
: 50;
}
function redactedActor(actor: {
id: string;
companyId: string;
name: string;
role: string;
title?: string | null;
status: string;
reportsTo?: string | null;
}) {
return {
id: actor.id,
companyId: actor.companyId,
name: actor.name,
role: actor.role,
title: actor.title ?? null,
status: actor.status,
reportsTo: actor.reportsTo ?? null,
};
}