mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-10-25 17:45:53 +01:00
Bind Paperclip Runner read tools
This commit is contained in:
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,
|
||||
};
|
||||
}
|
||||
Reference in new issue
Block a user