Skip to content
14 changes: 10 additions & 4 deletions hindsight-integrations/paperclip/src/bank.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,18 +3,20 @@
*
* Default format: "paperclip::{companyId}::{agentId}"
*
* bankGranularity: ['company'] → "paperclip::{companyId}"
* bankGranularity: ['agent'] → "paperclip::{agentId}"
* bankGranularity: ['company','agent'] → "paperclip::{companyId}::{agentId}"
* bankGranularity: ['company'] → "paperclip::{companyId}"
* bankGranularity: ['agent'] → "paperclip::{agentId}"
* bankGranularity: ['company','agent'] → "paperclip::{companyId}::{agentId}"
* bankGranularity: ['company','agent','user'] → "paperclip::{companyId}::{agentId}::user::{userId}"
*/

export interface BankContext {
companyId: string;
agentId: string;
userId?: string;
}

export interface BankConfig {
bankGranularity?: Array<"company" | "agent">;
bankGranularity?: Array<"company" | "agent" | "user">;
}

export function deriveBankId(context: BankContext, config: BankConfig): string {
Expand All @@ -24,6 +26,10 @@ export function deriveBankId(context: BankContext, config: BankConfig): string {
for (const field of granularity) {
if (field === "company") parts.push(context.companyId);
if (field === "agent") parts.push(context.agentId);
if (field === "user" && context.userId) {
parts.push("user");
parts.push(context.userId);
}
}

return parts.join("::");
Expand Down
5 changes: 3 additions & 2 deletions hindsight-integrations/paperclip/src/manifest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ const manifest: PaperclipPluginManifestV1 = {
"http.outbound",
"secrets.read-ref",
"agents.read",
"issues.read",
],
entrypoints: {
worker: "./dist/worker.js",
Expand All @@ -42,8 +43,8 @@ const manifest: PaperclipPluginManifestV1 = {
type: "array",
title: "Bank Granularity",
description:
"Controls memory isolation. Default ['company', 'agent'] gives each agent its own bank per company.",
items: { type: "string", enum: ["company", "agent"] },
"Controls memory isolation. Default ['company', 'agent'] gives each agent its own bank per company. Add 'user' for per-user memory isolation (GDPR compliance).",
items: { type: "string", enum: ["company", "agent", "user"] },
default: ["company", "agent"],
},
recallBudget: {
Expand Down
51 changes: 45 additions & 6 deletions hindsight-integrations/paperclip/src/worker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,14 +13,14 @@
*/

import { definePlugin, runWorker } from "@paperclipai/plugin-sdk";
import type { ToolRunContext } from "@paperclipai/plugin-sdk";
import type { PluginContext, ToolRunContext } from "@paperclipai/plugin-sdk";
import { HindsightClient, formatMemories } from "./client.js";
import { deriveBankId } from "./bank.js";

interface PluginConfig {
hindsightApiUrl: string;
hindsightApiKeyRef?: string;
bankGranularity?: Array<"company" | "agent">;
bankGranularity?: Array<"company" | "agent" | "user">;
recallBudget?: "low" | "mid" | "high";
autoRetain?: boolean;
}
Expand Down Expand Up @@ -54,6 +54,41 @@ async function resolveApiKey(
return resolved ?? undefined;
}

async function resolveUserIdFromActiveIssue(
ctx: PluginContext,
companyId: string,
agentId: string,
config: PluginConfig
): Promise<string | undefined> {
// Only resolve user ID if user granularity is enabled
if (!config.bankGranularity?.includes("user")) return undefined;

try {
const issues = await ctx.issues.list({
companyId,
assigneeAgentId: agentId,
status: "in_progress",
limit: 1,
});
if (!issues || issues.length === 0) return undefined;

const originId = issues[0]?.originId;
if (!originId) return undefined;

// Format: "channel-key::user-email" — extract the email
// Search backwards for a part that looks like an email (contains "@")
const parts = originId.split("::");
for (let i = parts.length - 1; i >= 0; i--) {
if (parts[i].includes("@")) {
return parts[i];
}
}
return undefined;
} catch (err) {
return undefined;
}
}

const plugin = definePlugin({
async setup(ctx) {
ctx.logger.info("Hindsight memory plugin starting");
Expand All @@ -66,14 +101,15 @@ const plugin = definePlugin({
const config = await getConfig(ctx);
const { agentId, runId, issueTitle, issueDescription } = payload;
const companyId = event.companyId;
const userId = await resolveUserIdFromActiveIssue(ctx, companyId, agentId, config);

const query = [issueTitle, issueDescription].filter(Boolean).join("\n");
if (!query.trim()) return;

try {
const apiKey = await resolveApiKey(ctx, config);
const client = new HindsightClient(config.hindsightApiUrl, apiKey);
const bankId = deriveBankId({ companyId, agentId }, config);
const bankId = deriveBankId({ companyId, agentId, userId }, config);

const response = await client.recall(bankId, query, config.recallBudget ?? "mid");

Expand Down Expand Up @@ -109,14 +145,15 @@ const plugin = definePlugin({

const { agentId, runId, output, result } = payload;
const companyId = event.companyId;
const userId = await resolveUserIdFromActiveIssue(ctx, companyId, agentId, config);
const content = output ?? result;

if (!content?.trim()) return;

try {
const apiKey = await resolveApiKey(ctx, config);
const client = new HindsightClient(config.hindsightApiUrl, apiKey);
const bankId = deriveBankId({ companyId, agentId }, config);
const bankId = deriveBankId({ companyId, agentId, userId }, config);

await client.retain(bankId, content, runId, { agentId, companyId, runId });
ctx.logger.info("Retained run output to memory", { runId, bankId });
Expand Down Expand Up @@ -147,8 +184,9 @@ const plugin = definePlugin({
async (params: unknown, runCtx: ToolRunContext) => {
const { query } = params as { query: string };
const config = await getConfig(ctx);
const userId = await resolveUserIdFromActiveIssue(ctx, runCtx.companyId, runCtx.agentId, config);
const bankId = deriveBankId(
{ companyId: runCtx.companyId, agentId: runCtx.agentId },
{ companyId: runCtx.companyId, agentId: runCtx.agentId, userId },
config
);

Expand Down Expand Up @@ -198,8 +236,9 @@ const plugin = definePlugin({
async (params: unknown, runCtx: ToolRunContext) => {
const { content } = params as { content: string };
const config = await getConfig(ctx);
const userId = await resolveUserIdFromActiveIssue(ctx, runCtx.companyId, runCtx.agentId, config);
const bankId = deriveBankId(
{ companyId: runCtx.companyId, agentId: runCtx.agentId },
{ companyId: runCtx.companyId, agentId: runCtx.agentId, userId },
config
);

Expand Down
Loading