mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-10-25 17:45:53 +01:00
Bind Runner human interactions
This commit is contained in:
1 parent
3485f2f775
commit
37c09c82de
2 files changed
+89
-2
No files matched your search
@@ -1,6 +1,6 @@
|
||||
import { afterAll, beforeAll, describe, expect, it } from "vitest";
|
||||
import { eq } from "drizzle-orm";
|
||||
import { agents, companies, createDb, heartbeatRuns, issueComments, issues } from "@paperclipai/db";
|
||||
import { agents, companies, createDb, heartbeatRuns, issueComments, issueThreadInteractions, issues } from "@paperclipai/db";
|
||||
import { startEmbeddedPostgresTestDatabase } from "../../__tests__/helpers/embedded-postgres.js";
|
||||
import { PaperclipRunnerToolAuthority } from "./paperclip-runner-tool-authority.js";
|
||||
|
||||
@@ -53,9 +53,10 @@ 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()).toHaveLength(12);
|
||||
expect(authority.definitions()).toHaveLength(13);
|
||||
expect(authority.definitions().map((tool) => tool.name)).toEqual(expect.arrayContaining([
|
||||
"get_task_context", "get_task_history", "search_tasks", "report_progress",
|
||||
"request_human_input",
|
||||
"list_documents", "read_document", "list_document_revisions",
|
||||
"list_agents", "get_agent", "list_approvals", "get_approval", "get_approval_context",
|
||||
]));
|
||||
@@ -83,6 +84,31 @@ describe("PaperclipRunnerToolAuthority", () => {
|
||||
})).rejects.toThrow("paperclip_runner_tool_idempotency_conflict");
|
||||
});
|
||||
|
||||
it("creates checkbox interactions through the real interaction service", async () => {
|
||||
const authority = new PaperclipRunnerToolAuthority(db, { companyId, agentId, issueId, runId });
|
||||
await expect(authority.execute({
|
||||
tool: "request_human_input",
|
||||
callId: "ask-checkbox",
|
||||
arguments: {
|
||||
idempotencyKey: "favorite-animals",
|
||||
interactionKind: "checkbox",
|
||||
title: "Favorite zoo animals",
|
||||
prompt: "Which zoo animals are your favorites?",
|
||||
continuationPolicy: "wake_assignee",
|
||||
payload: {
|
||||
options: [
|
||||
{ id: "giraffes", label: "Giraffes" },
|
||||
{ id: "lions", label: "Lions" },
|
||||
],
|
||||
},
|
||||
},
|
||||
})).resolves.toMatchObject({
|
||||
interaction: { kind: "request_checkbox_confirmation", status: "pending" },
|
||||
});
|
||||
expect(await db.select().from(issueThreadInteractions).where(eq(issueThreadInteractions.issueId, issueId)))
|
||||
.toHaveLength(1);
|
||||
});
|
||||
|
||||
it("fails closed once the run is no longer active", async () => {
|
||||
await db.update(heartbeatRuns).set({ status: "succeeded" }).where(eq(heartbeatRuns.id, runId));
|
||||
const authority = new PaperclipRunnerToolAuthority(db, { companyId, agentId, issueId, runId });
|
||||
|
||||
@@ -6,9 +6,12 @@ import { agentService } from "../agents.js";
|
||||
import { approvalService } from "../approvals.js";
|
||||
import { documentService } from "../documents.js";
|
||||
import { issueService } from "../issues.js";
|
||||
import { issueThreadInteractionService } from "../issue-thread-interactions.js";
|
||||
import { persistActivity, publishActivity } from "../activity-log.js";
|
||||
|
||||
const IMPLEMENTED_OPERATIONS = new Set([
|
||||
"get_task_context", "get_task_history", "search_tasks", "report_progress",
|
||||
"request_human_input",
|
||||
"list_documents", "read_document", "list_document_revisions",
|
||||
"list_agents", "get_agent", "list_approvals", "get_approval", "get_approval_context",
|
||||
]);
|
||||
@@ -110,6 +113,7 @@ export class PaperclipRunnerToolAuthority {
|
||||
return { approval, tasks: tasks.map((row) => row.issue) };
|
||||
}
|
||||
case "report_progress": return this.#reportProgress(call.callId, call.arguments);
|
||||
case "request_human_input": return this.#requestHumanInput(input, context.issue);
|
||||
default: throw new Error("paperclip_runner_tool_not_bound");
|
||||
}
|
||||
}
|
||||
@@ -178,6 +182,63 @@ export class PaperclipRunnerToolAuthority {
|
||||
return result;
|
||||
});
|
||||
}
|
||||
|
||||
async #requestHumanInput(input: Record<string, unknown>, issue: typeof issues.$inferSelect): Promise<unknown> {
|
||||
const interactionKind = requiredString(input.interactionKind);
|
||||
const kind = ({
|
||||
confirmation: "request_confirmation",
|
||||
checkbox: "request_checkbox_confirmation",
|
||||
questions: "ask_user_questions",
|
||||
suggest_tasks: "suggest_tasks",
|
||||
item_verdicts: "request_item_verdicts",
|
||||
} as const)[interactionKind as "confirmation"];
|
||||
if (!kind) throw new Error("paperclip_runner_interaction_kind_invalid");
|
||||
const suppliedPayload = record(input.payload);
|
||||
if (input.targetRevisionId && suppliedPayload.target === undefined) {
|
||||
throw new Error("paperclip_runner_interaction_target_incomplete");
|
||||
}
|
||||
const prompt = requiredString(input.prompt);
|
||||
const interaction = await issueThreadInteractionService(this.db).create(issue, {
|
||||
kind,
|
||||
idempotencyKey: requiredString(input.idempotencyKey),
|
||||
sourceRunId: this.binding.runId,
|
||||
title: requiredString(input.title),
|
||||
summary: prompt,
|
||||
continuationPolicy: requiredString(input.continuationPolicy),
|
||||
payload: {
|
||||
...suppliedPayload,
|
||||
version: 1,
|
||||
prompt,
|
||||
...(kind === "request_confirmation" ? {
|
||||
detailsMarkdown: suppliedPayload.detailsMarkdown ?? "",
|
||||
acceptLabel: suppliedPayload.acceptLabel ?? "Confirm",
|
||||
rejectLabel: suppliedPayload.rejectLabel ?? "Request changes",
|
||||
rejectRequiresReason: suppliedPayload.rejectRequiresReason ?? false,
|
||||
supersedeOnUserComment: suppliedPayload.supersedeOnUserComment ?? true,
|
||||
} : {}),
|
||||
},
|
||||
} as never, { agentId: this.binding.agentId, userId: null });
|
||||
const activity = await persistActivity(this.db, {
|
||||
companyId: this.binding.companyId,
|
||||
actorType: "agent",
|
||||
actorId: this.binding.agentId,
|
||||
agentId: this.binding.agentId,
|
||||
runId: this.binding.runId,
|
||||
issueId: this.binding.issueId,
|
||||
action: "issue.thread_interaction_created",
|
||||
entityType: "issue",
|
||||
entityId: this.binding.issueId,
|
||||
details: {
|
||||
interactionId: interaction.id,
|
||||
interactionKind: interaction.kind,
|
||||
interactionStatus: interaction.status,
|
||||
continuationPolicy: interaction.continuationPolicy,
|
||||
source: "paperclip_runner_protocol",
|
||||
},
|
||||
});
|
||||
publishActivity(activity.publication);
|
||||
return { interaction, disposition: "applied" };
|
||||
}
|
||||
}
|
||||
|
||||
function requiredString(value: unknown): string {
|
||||
|
||||
Reference in new issue
Block a user