mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-11-04 19:28:46 +01:00
feat: implement memory service end to end
This commit is contained in:
1 parent
b649bd454f
commit
c3238242e8
48 files changed
+17769
-2
No files matched your search
@@ -410,10 +410,12 @@ export async function execute(ctx: AdapterExecutionContext): Promise<AdapterExec
|
||||
const wakePrompt = renderPaperclipWakePrompt(context.paperclipWake, { resumedSession: Boolean(sessionId) });
|
||||
const shouldUseResumeDeltaPrompt = Boolean(sessionId) && wakePrompt.length > 0;
|
||||
const renderedPrompt = shouldUseResumeDeltaPrompt ? "" : renderTemplate(promptTemplate, templateData);
|
||||
const memoryPreamble = asString(context.paperclipMemoryPreamble, "").trim();
|
||||
const sessionHandoffNote = asString(context.paperclipSessionHandoffMarkdown, "").trim();
|
||||
const prompt = joinPromptSections([
|
||||
renderedBootstrapPrompt,
|
||||
wakePrompt,
|
||||
memoryPreamble,
|
||||
sessionHandoffNote,
|
||||
renderedPrompt,
|
||||
]);
|
||||
@@ -421,6 +423,7 @@ export async function execute(ctx: AdapterExecutionContext): Promise<AdapterExec
|
||||
promptChars: prompt.length,
|
||||
bootstrapPromptChars: renderedBootstrapPrompt.length,
|
||||
wakePromptChars: wakePrompt.length,
|
||||
memoryPreambleChars: memoryPreamble.length,
|
||||
sessionHandoffChars: sessionHandoffNote.length,
|
||||
heartbeatPromptChars: renderedPrompt.length,
|
||||
};
|
||||
|
||||
@@ -467,11 +467,13 @@ export async function execute(ctx: AdapterExecutionContext): Promise<AdapterExec
|
||||
];
|
||||
})();
|
||||
const renderedPrompt = shouldUseResumeDeltaPrompt ? "" : renderTemplate(promptTemplate, templateData);
|
||||
const memoryPreamble = asString(context.paperclipMemoryPreamble, "").trim();
|
||||
const sessionHandoffNote = asString(context.paperclipSessionHandoffMarkdown, "").trim();
|
||||
const prompt = joinPromptSections([
|
||||
promptInstructionsPrefix,
|
||||
renderedBootstrapPrompt,
|
||||
wakePrompt,
|
||||
memoryPreamble,
|
||||
sessionHandoffNote,
|
||||
renderedPrompt,
|
||||
]);
|
||||
@@ -480,6 +482,7 @@ export async function execute(ctx: AdapterExecutionContext): Promise<AdapterExec
|
||||
instructionsChars,
|
||||
bootstrapPromptChars: renderedBootstrapPrompt.length,
|
||||
wakePromptChars: wakePrompt.length,
|
||||
memoryPreambleChars: memoryPreamble.length,
|
||||
sessionHandoffChars: sessionHandoffNote.length,
|
||||
heartbeatPromptChars: renderedPrompt.length,
|
||||
};
|
||||
|
||||
@@ -365,12 +365,14 @@ export async function execute(ctx: AdapterExecutionContext): Promise<AdapterExec
|
||||
const wakePrompt = renderPaperclipWakePrompt(context.paperclipWake, { resumedSession: Boolean(sessionId) });
|
||||
const shouldUseResumeDeltaPrompt = Boolean(sessionId) && wakePrompt.length > 0;
|
||||
const renderedPrompt = shouldUseResumeDeltaPrompt ? "" : renderTemplate(promptTemplate, templateData);
|
||||
const memoryPreamble = asString(context.paperclipMemoryPreamble, "").trim();
|
||||
const sessionHandoffNote = asString(context.paperclipSessionHandoffMarkdown, "").trim();
|
||||
const paperclipEnvNote = renderPaperclipEnvNote(env);
|
||||
const prompt = joinPromptSections([
|
||||
instructionsPrefix,
|
||||
renderedBootstrapPrompt,
|
||||
wakePrompt,
|
||||
memoryPreamble,
|
||||
sessionHandoffNote,
|
||||
paperclipEnvNote,
|
||||
renderedPrompt,
|
||||
@@ -380,6 +382,7 @@ export async function execute(ctx: AdapterExecutionContext): Promise<AdapterExec
|
||||
instructionsChars,
|
||||
bootstrapPromptChars: renderedBootstrapPrompt.length,
|
||||
wakePromptChars: wakePrompt.length,
|
||||
memoryPreambleChars: memoryPreamble.length,
|
||||
sessionHandoffChars: sessionHandoffNote.length,
|
||||
runtimeNoteChars: paperclipEnvNote.length,
|
||||
heartbeatPromptChars: renderedPrompt.length,
|
||||
|
||||
@@ -306,6 +306,7 @@ export async function execute(ctx: AdapterExecutionContext): Promise<AdapterExec
|
||||
const wakePrompt = renderPaperclipWakePrompt(context.paperclipWake, { resumedSession: Boolean(sessionId) });
|
||||
const shouldUseResumeDeltaPrompt = Boolean(sessionId) && wakePrompt.length > 0;
|
||||
const renderedPrompt = shouldUseResumeDeltaPrompt ? "" : renderTemplate(promptTemplate, templateData);
|
||||
const memoryPreamble = asString(context.paperclipMemoryPreamble, "").trim();
|
||||
const sessionHandoffNote = asString(context.paperclipSessionHandoffMarkdown, "").trim();
|
||||
const paperclipEnvNote = renderPaperclipEnvNote(env);
|
||||
const apiAccessNote = renderApiAccessNote(env);
|
||||
@@ -313,6 +314,7 @@ export async function execute(ctx: AdapterExecutionContext): Promise<AdapterExec
|
||||
instructionsPrefix,
|
||||
renderedBootstrapPrompt,
|
||||
wakePrompt,
|
||||
memoryPreamble,
|
||||
sessionHandoffNote,
|
||||
paperclipEnvNote,
|
||||
apiAccessNote,
|
||||
@@ -323,6 +325,7 @@ export async function execute(ctx: AdapterExecutionContext): Promise<AdapterExec
|
||||
instructionsChars: instructionsPrefix.length,
|
||||
bootstrapPromptChars: renderedBootstrapPrompt.length,
|
||||
wakePromptChars: wakePrompt.length,
|
||||
memoryPreambleChars: memoryPreamble.length,
|
||||
sessionHandoffChars: sessionHandoffNote.length,
|
||||
runtimeNoteChars: paperclipEnvNote.length + apiAccessNote.length,
|
||||
heartbeatPromptChars: renderedPrompt.length,
|
||||
|
||||
@@ -281,11 +281,13 @@ export async function execute(ctx: AdapterExecutionContext): Promise<AdapterExec
|
||||
const wakePrompt = renderPaperclipWakePrompt(context.paperclipWake, { resumedSession: Boolean(sessionId) });
|
||||
const shouldUseResumeDeltaPrompt = Boolean(sessionId) && wakePrompt.length > 0;
|
||||
const renderedPrompt = shouldUseResumeDeltaPrompt ? "" : renderTemplate(promptTemplate, templateData);
|
||||
const memoryPreamble = asString(context.paperclipMemoryPreamble, "").trim();
|
||||
const sessionHandoffNote = asString(context.paperclipSessionHandoffMarkdown, "").trim();
|
||||
const prompt = joinPromptSections([
|
||||
instructionsPrefix,
|
||||
renderedBootstrapPrompt,
|
||||
wakePrompt,
|
||||
memoryPreamble,
|
||||
sessionHandoffNote,
|
||||
renderedPrompt,
|
||||
]);
|
||||
@@ -294,6 +296,7 @@ export async function execute(ctx: AdapterExecutionContext): Promise<AdapterExec
|
||||
instructionsChars: instructionsPrefix.length,
|
||||
bootstrapPromptChars: renderedBootstrapPrompt.length,
|
||||
wakePromptChars: wakePrompt.length,
|
||||
memoryPreambleChars: memoryPreamble.length,
|
||||
sessionHandoffChars: sessionHandoffNote.length,
|
||||
heartbeatPromptChars: renderedPrompt.length,
|
||||
};
|
||||
|
||||
@@ -309,10 +309,12 @@ export async function execute(ctx: AdapterExecutionContext): Promise<AdapterExec
|
||||
const wakePrompt = renderPaperclipWakePrompt(context.paperclipWake, { resumedSession: canResumeSession });
|
||||
const shouldUseResumeDeltaPrompt = canResumeSession && wakePrompt.length > 0;
|
||||
const renderedHeartbeatPrompt = shouldUseResumeDeltaPrompt ? "" : renderTemplate(promptTemplate, templateData);
|
||||
const memoryPreamble = asString(context.paperclipMemoryPreamble, "").trim();
|
||||
const sessionHandoffNote = asString(context.paperclipSessionHandoffMarkdown, "").trim();
|
||||
const userPrompt = joinPromptSections([
|
||||
renderedBootstrapPrompt,
|
||||
wakePrompt,
|
||||
memoryPreamble,
|
||||
sessionHandoffNote,
|
||||
renderedHeartbeatPrompt,
|
||||
]);
|
||||
@@ -321,6 +323,7 @@ export async function execute(ctx: AdapterExecutionContext): Promise<AdapterExec
|
||||
promptChars: userPrompt.length,
|
||||
bootstrapPromptChars: renderedBootstrapPrompt.length,
|
||||
wakePromptChars: wakePrompt.length,
|
||||
memoryPreambleChars: memoryPreamble.length,
|
||||
sessionHandoffChars: sessionHandoffNote.length,
|
||||
heartbeatPromptChars: renderedHeartbeatPrompt.length,
|
||||
};
|
||||
|
||||
@@ -0,0 +1,150 @@
|
||||
CREATE TABLE "memory_binding_targets" (
|
||||
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
|
||||
"company_id" uuid NOT NULL,
|
||||
"binding_id" uuid NOT NULL,
|
||||
"target_type" text NOT NULL,
|
||||
"target_id" uuid NOT NULL,
|
||||
"created_at" timestamp with time zone DEFAULT now() NOT NULL,
|
||||
"updated_at" timestamp with time zone DEFAULT now() NOT NULL
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE TABLE "memory_bindings" (
|
||||
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
|
||||
"company_id" uuid NOT NULL,
|
||||
"key" text NOT NULL,
|
||||
"name" text,
|
||||
"provider_key" text NOT NULL,
|
||||
"config" jsonb DEFAULT '{}'::jsonb NOT NULL,
|
||||
"enabled" boolean DEFAULT true NOT NULL,
|
||||
"created_at" timestamp with time zone DEFAULT now() NOT NULL,
|
||||
"updated_at" timestamp with time zone DEFAULT now() NOT NULL
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE TABLE "memory_extraction_jobs" (
|
||||
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
|
||||
"company_id" uuid NOT NULL,
|
||||
"binding_id" uuid NOT NULL,
|
||||
"provider_key" text NOT NULL,
|
||||
"operation_id" uuid,
|
||||
"status" text DEFAULT 'queued' NOT NULL,
|
||||
"provider_job_id" text,
|
||||
"source_kind" text,
|
||||
"source_issue_id" uuid,
|
||||
"source_comment_id" uuid,
|
||||
"source_document_key" text,
|
||||
"source_run_id" uuid,
|
||||
"source_activity_id" uuid,
|
||||
"source_external_ref" text,
|
||||
"result_json" jsonb,
|
||||
"error" text,
|
||||
"started_at" timestamp with time zone,
|
||||
"completed_at" timestamp with time zone,
|
||||
"created_at" timestamp with time zone DEFAULT now() NOT NULL,
|
||||
"updated_at" timestamp with time zone DEFAULT now() NOT NULL
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE TABLE "memory_local_records" (
|
||||
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
|
||||
"company_id" uuid NOT NULL,
|
||||
"binding_id" uuid NOT NULL,
|
||||
"provider_key" text NOT NULL,
|
||||
"scope_agent_id" uuid,
|
||||
"scope_project_id" uuid,
|
||||
"scope_issue_id" uuid,
|
||||
"scope_run_id" uuid,
|
||||
"scope_subject_id" text,
|
||||
"source_kind" text,
|
||||
"source_issue_id" uuid,
|
||||
"source_comment_id" uuid,
|
||||
"source_document_key" text,
|
||||
"source_run_id" uuid,
|
||||
"source_activity_id" uuid,
|
||||
"source_external_ref" text,
|
||||
"title" text,
|
||||
"content" text NOT NULL,
|
||||
"summary" text,
|
||||
"metadata" jsonb DEFAULT '{}'::jsonb NOT NULL,
|
||||
"created_by_operation_id" uuid,
|
||||
"deleted_at" timestamp with time zone,
|
||||
"created_at" timestamp with time zone DEFAULT now() NOT NULL,
|
||||
"updated_at" timestamp with time zone DEFAULT now() NOT NULL
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE TABLE "memory_operations" (
|
||||
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
|
||||
"company_id" uuid NOT NULL,
|
||||
"binding_id" uuid NOT NULL,
|
||||
"provider_key" text NOT NULL,
|
||||
"operation_type" text NOT NULL,
|
||||
"trigger_kind" text DEFAULT 'manual' NOT NULL,
|
||||
"hook_kind" text,
|
||||
"status" text DEFAULT 'succeeded' NOT NULL,
|
||||
"actor_type" text NOT NULL,
|
||||
"actor_id" text NOT NULL,
|
||||
"agent_id" uuid,
|
||||
"user_id" text,
|
||||
"scope_agent_id" uuid,
|
||||
"scope_project_id" uuid,
|
||||
"scope_issue_id" uuid,
|
||||
"scope_run_id" uuid,
|
||||
"scope_subject_id" text,
|
||||
"source_kind" text,
|
||||
"source_issue_id" uuid,
|
||||
"source_comment_id" uuid,
|
||||
"source_document_key" text,
|
||||
"source_run_id" uuid,
|
||||
"source_activity_id" uuid,
|
||||
"source_external_ref" text,
|
||||
"query_text" text,
|
||||
"record_count" integer DEFAULT 0 NOT NULL,
|
||||
"request_json" jsonb,
|
||||
"result_json" jsonb,
|
||||
"usage_json" jsonb DEFAULT '[]'::jsonb NOT NULL,
|
||||
"error" text,
|
||||
"cost_event_id" uuid,
|
||||
"finance_event_id" uuid,
|
||||
"occurred_at" timestamp with time zone DEFAULT now() NOT NULL,
|
||||
"created_at" timestamp with time zone DEFAULT now() NOT NULL
|
||||
);
|
||||
--> statement-breakpoint
|
||||
ALTER TABLE "memory_binding_targets" ADD CONSTRAINT "memory_binding_targets_company_id_companies_id_fk" FOREIGN KEY ("company_id") REFERENCES "public"."companies"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
|
||||
ALTER TABLE "memory_binding_targets" ADD CONSTRAINT "memory_binding_targets_binding_id_memory_bindings_id_fk" FOREIGN KEY ("binding_id") REFERENCES "public"."memory_bindings"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
|
||||
ALTER TABLE "memory_bindings" ADD CONSTRAINT "memory_bindings_company_id_companies_id_fk" FOREIGN KEY ("company_id") REFERENCES "public"."companies"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
|
||||
ALTER TABLE "memory_extraction_jobs" ADD CONSTRAINT "memory_extraction_jobs_company_id_companies_id_fk" FOREIGN KEY ("company_id") REFERENCES "public"."companies"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
|
||||
ALTER TABLE "memory_extraction_jobs" ADD CONSTRAINT "memory_extraction_jobs_binding_id_memory_bindings_id_fk" FOREIGN KEY ("binding_id") REFERENCES "public"."memory_bindings"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
|
||||
ALTER TABLE "memory_extraction_jobs" ADD CONSTRAINT "memory_extraction_jobs_operation_id_memory_operations_id_fk" FOREIGN KEY ("operation_id") REFERENCES "public"."memory_operations"("id") ON DELETE set null ON UPDATE no action;--> statement-breakpoint
|
||||
ALTER TABLE "memory_local_records" ADD CONSTRAINT "memory_local_records_company_id_companies_id_fk" FOREIGN KEY ("company_id") REFERENCES "public"."companies"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
|
||||
ALTER TABLE "memory_local_records" ADD CONSTRAINT "memory_local_records_binding_id_memory_bindings_id_fk" FOREIGN KEY ("binding_id") REFERENCES "public"."memory_bindings"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
|
||||
ALTER TABLE "memory_local_records" ADD CONSTRAINT "memory_local_records_scope_agent_id_agents_id_fk" FOREIGN KEY ("scope_agent_id") REFERENCES "public"."agents"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint
|
||||
ALTER TABLE "memory_local_records" ADD CONSTRAINT "memory_local_records_scope_project_id_projects_id_fk" FOREIGN KEY ("scope_project_id") REFERENCES "public"."projects"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint
|
||||
ALTER TABLE "memory_local_records" ADD CONSTRAINT "memory_local_records_scope_issue_id_issues_id_fk" FOREIGN KEY ("scope_issue_id") REFERENCES "public"."issues"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint
|
||||
ALTER TABLE "memory_local_records" ADD CONSTRAINT "memory_local_records_scope_run_id_heartbeat_runs_id_fk" FOREIGN KEY ("scope_run_id") REFERENCES "public"."heartbeat_runs"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint
|
||||
ALTER TABLE "memory_local_records" ADD CONSTRAINT "memory_local_records_source_issue_id_issues_id_fk" FOREIGN KEY ("source_issue_id") REFERENCES "public"."issues"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint
|
||||
ALTER TABLE "memory_local_records" ADD CONSTRAINT "memory_local_records_source_run_id_heartbeat_runs_id_fk" FOREIGN KEY ("source_run_id") REFERENCES "public"."heartbeat_runs"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint
|
||||
ALTER TABLE "memory_local_records" ADD CONSTRAINT "memory_local_records_created_by_operation_id_memory_operations_id_fk" FOREIGN KEY ("created_by_operation_id") REFERENCES "public"."memory_operations"("id") ON DELETE set null ON UPDATE no action;--> statement-breakpoint
|
||||
ALTER TABLE "memory_operations" ADD CONSTRAINT "memory_operations_company_id_companies_id_fk" FOREIGN KEY ("company_id") REFERENCES "public"."companies"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
|
||||
ALTER TABLE "memory_operations" ADD CONSTRAINT "memory_operations_binding_id_memory_bindings_id_fk" FOREIGN KEY ("binding_id") REFERENCES "public"."memory_bindings"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
|
||||
ALTER TABLE "memory_operations" ADD CONSTRAINT "memory_operations_agent_id_agents_id_fk" FOREIGN KEY ("agent_id") REFERENCES "public"."agents"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint
|
||||
ALTER TABLE "memory_operations" ADD CONSTRAINT "memory_operations_scope_agent_id_agents_id_fk" FOREIGN KEY ("scope_agent_id") REFERENCES "public"."agents"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint
|
||||
ALTER TABLE "memory_operations" ADD CONSTRAINT "memory_operations_scope_project_id_projects_id_fk" FOREIGN KEY ("scope_project_id") REFERENCES "public"."projects"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint
|
||||
ALTER TABLE "memory_operations" ADD CONSTRAINT "memory_operations_scope_issue_id_issues_id_fk" FOREIGN KEY ("scope_issue_id") REFERENCES "public"."issues"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint
|
||||
ALTER TABLE "memory_operations" ADD CONSTRAINT "memory_operations_scope_run_id_heartbeat_runs_id_fk" FOREIGN KEY ("scope_run_id") REFERENCES "public"."heartbeat_runs"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint
|
||||
ALTER TABLE "memory_operations" ADD CONSTRAINT "memory_operations_source_issue_id_issues_id_fk" FOREIGN KEY ("source_issue_id") REFERENCES "public"."issues"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint
|
||||
ALTER TABLE "memory_operations" ADD CONSTRAINT "memory_operations_source_comment_id_issue_comments_id_fk" FOREIGN KEY ("source_comment_id") REFERENCES "public"."issue_comments"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint
|
||||
ALTER TABLE "memory_operations" ADD CONSTRAINT "memory_operations_source_run_id_heartbeat_runs_id_fk" FOREIGN KEY ("source_run_id") REFERENCES "public"."heartbeat_runs"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint
|
||||
ALTER TABLE "memory_operations" ADD CONSTRAINT "memory_operations_source_activity_id_activity_log_id_fk" FOREIGN KEY ("source_activity_id") REFERENCES "public"."activity_log"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint
|
||||
ALTER TABLE "memory_operations" ADD CONSTRAINT "memory_operations_cost_event_id_cost_events_id_fk" FOREIGN KEY ("cost_event_id") REFERENCES "public"."cost_events"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint
|
||||
ALTER TABLE "memory_operations" ADD CONSTRAINT "memory_operations_finance_event_id_finance_events_id_fk" FOREIGN KEY ("finance_event_id") REFERENCES "public"."finance_events"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint
|
||||
CREATE UNIQUE INDEX "memory_binding_targets_company_target_idx" ON "memory_binding_targets" USING btree ("company_id","target_type","target_id");--> statement-breakpoint
|
||||
CREATE INDEX "memory_binding_targets_company_binding_idx" ON "memory_binding_targets" USING btree ("company_id","binding_id");--> statement-breakpoint
|
||||
CREATE UNIQUE INDEX "memory_bindings_company_key_idx" ON "memory_bindings" USING btree ("company_id","key");--> statement-breakpoint
|
||||
CREATE INDEX "memory_bindings_company_provider_idx" ON "memory_bindings" USING btree ("company_id","provider_key");--> statement-breakpoint
|
||||
CREATE INDEX "memory_extraction_jobs_company_status_created_idx" ON "memory_extraction_jobs" USING btree ("company_id","status","created_at");--> statement-breakpoint
|
||||
CREATE INDEX "memory_extraction_jobs_company_binding_created_idx" ON "memory_extraction_jobs" USING btree ("company_id","binding_id","created_at");--> statement-breakpoint
|
||||
CREATE INDEX "memory_local_records_company_binding_created_idx" ON "memory_local_records" USING btree ("company_id","binding_id","created_at");--> statement-breakpoint
|
||||
CREATE INDEX "memory_local_records_company_agent_created_idx" ON "memory_local_records" USING btree ("company_id","scope_agent_id","created_at");--> statement-breakpoint
|
||||
CREATE INDEX "memory_local_records_company_issue_created_idx" ON "memory_local_records" USING btree ("company_id","scope_issue_id","created_at");--> statement-breakpoint
|
||||
CREATE INDEX "memory_operations_company_occurred_idx" ON "memory_operations" USING btree ("company_id","occurred_at");--> statement-breakpoint
|
||||
CREATE INDEX "memory_operations_company_binding_occurred_idx" ON "memory_operations" USING btree ("company_id","binding_id","occurred_at");--> statement-breakpoint
|
||||
CREATE INDEX "memory_operations_company_issue_occurred_idx" ON "memory_operations" USING btree ("company_id","scope_issue_id","occurred_at");--> statement-breakpoint
|
||||
CREATE INDEX "memory_operations_company_run_occurred_idx" ON "memory_operations" USING btree ("company_id","scope_run_id","occurred_at");
|
||||
File diff suppressed because it is too large.
Load diff
@@ -393,6 +393,13 @@
|
||||
"when": 1775825256196,
|
||||
"tag": "0055_kind_weapon_omega",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 56,
|
||||
"version": "7",
|
||||
"when": 1775925298474,
|
||||
"tag": "0056_oval_gauntlet",
|
||||
"breakpoints": true
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -53,6 +53,11 @@ export { activityLog } from "./activity_log.js";
|
||||
export { companySecrets } from "./company_secrets.js";
|
||||
export { companySecretVersions } from "./company_secret_versions.js";
|
||||
export { companySkills } from "./company_skills.js";
|
||||
export { memoryBindings } from "./memory_bindings.js";
|
||||
export { memoryBindingTargets } from "./memory_binding_targets.js";
|
||||
export { memoryOperations } from "./memory_operations.js";
|
||||
export { memoryExtractionJobs } from "./memory_extraction_jobs.js";
|
||||
export { memoryLocalRecords } from "./memory_local_records.js";
|
||||
export { plugins } from "./plugins.js";
|
||||
export { pluginConfig } from "./plugin_config.js";
|
||||
export { pluginCompanySettings } from "./plugin_company_settings.js";
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
import { pgTable, uuid, text, timestamp, uniqueIndex, index } from "drizzle-orm/pg-core";
|
||||
import { companies } from "./companies.js";
|
||||
import { memoryBindings } from "./memory_bindings.js";
|
||||
import type { MemoryBindingTargetType } from "@paperclipai/shared";
|
||||
|
||||
export const memoryBindingTargets = pgTable(
|
||||
"memory_binding_targets",
|
||||
{
|
||||
id: uuid("id").primaryKey().defaultRandom(),
|
||||
companyId: uuid("company_id").notNull().references(() => companies.id, { onDelete: "cascade" }),
|
||||
bindingId: uuid("binding_id").notNull().references(() => memoryBindings.id, { onDelete: "cascade" }),
|
||||
targetType: text("target_type").$type<MemoryBindingTargetType>().notNull(),
|
||||
targetId: uuid("target_id").notNull(),
|
||||
createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
|
||||
updatedAt: timestamp("updated_at", { withTimezone: true }).notNull().defaultNow(),
|
||||
},
|
||||
(table) => ({
|
||||
companyTargetUniqueIdx: uniqueIndex("memory_binding_targets_company_target_idx").on(
|
||||
table.companyId,
|
||||
table.targetType,
|
||||
table.targetId,
|
||||
),
|
||||
companyBindingIdx: index("memory_binding_targets_company_binding_idx").on(table.companyId, table.bindingId),
|
||||
}),
|
||||
);
|
||||
@@ -0,0 +1,21 @@
|
||||
import { pgTable, uuid, text, timestamp, boolean, jsonb, uniqueIndex, index } from "drizzle-orm/pg-core";
|
||||
import { companies } from "./companies.js";
|
||||
|
||||
export const memoryBindings = pgTable(
|
||||
"memory_bindings",
|
||||
{
|
||||
id: uuid("id").primaryKey().defaultRandom(),
|
||||
companyId: uuid("company_id").notNull().references(() => companies.id, { onDelete: "cascade" }),
|
||||
key: text("key").notNull(),
|
||||
name: text("name"),
|
||||
providerKey: text("provider_key").notNull(),
|
||||
config: jsonb("config").$type<Record<string, unknown>>().notNull().default({}),
|
||||
enabled: boolean("enabled").notNull().default(true),
|
||||
createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
|
||||
updatedAt: timestamp("updated_at", { withTimezone: true }).notNull().defaultNow(),
|
||||
},
|
||||
(table) => ({
|
||||
companyKeyUniqueIdx: uniqueIndex("memory_bindings_company_key_idx").on(table.companyId, table.key),
|
||||
companyProviderIdx: index("memory_bindings_company_provider_idx").on(table.companyId, table.providerKey),
|
||||
}),
|
||||
);
|
||||
@@ -0,0 +1,43 @@
|
||||
import { pgTable, uuid, text, timestamp, jsonb, index } from "drizzle-orm/pg-core";
|
||||
import { companies } from "./companies.js";
|
||||
import { memoryBindings } from "./memory_bindings.js";
|
||||
import { memoryOperations } from "./memory_operations.js";
|
||||
import type { MemoryExtractionJobStatus, MemorySourceKind } from "@paperclipai/shared";
|
||||
|
||||
export const memoryExtractionJobs = pgTable(
|
||||
"memory_extraction_jobs",
|
||||
{
|
||||
id: uuid("id").primaryKey().defaultRandom(),
|
||||
companyId: uuid("company_id").notNull().references(() => companies.id, { onDelete: "cascade" }),
|
||||
bindingId: uuid("binding_id").notNull().references(() => memoryBindings.id, { onDelete: "cascade" }),
|
||||
providerKey: text("provider_key").notNull(),
|
||||
operationId: uuid("operation_id").references(() => memoryOperations.id, { onDelete: "set null" }),
|
||||
status: text("status").$type<MemoryExtractionJobStatus>().notNull().default("queued"),
|
||||
providerJobId: text("provider_job_id"),
|
||||
sourceKind: text("source_kind").$type<MemorySourceKind>(),
|
||||
sourceIssueId: uuid("source_issue_id"),
|
||||
sourceCommentId: uuid("source_comment_id"),
|
||||
sourceDocumentKey: text("source_document_key"),
|
||||
sourceRunId: uuid("source_run_id"),
|
||||
sourceActivityId: uuid("source_activity_id"),
|
||||
sourceExternalRef: text("source_external_ref"),
|
||||
resultJson: jsonb("result_json").$type<Record<string, unknown> | null>(),
|
||||
error: text("error"),
|
||||
startedAt: timestamp("started_at", { withTimezone: true }),
|
||||
completedAt: timestamp("completed_at", { withTimezone: true }),
|
||||
createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
|
||||
updatedAt: timestamp("updated_at", { withTimezone: true }).notNull().defaultNow(),
|
||||
},
|
||||
(table) => ({
|
||||
companyStatusCreatedIdx: index("memory_extraction_jobs_company_status_created_idx").on(
|
||||
table.companyId,
|
||||
table.status,
|
||||
table.createdAt,
|
||||
),
|
||||
companyBindingCreatedIdx: index("memory_extraction_jobs_company_binding_created_idx").on(
|
||||
table.companyId,
|
||||
table.bindingId,
|
||||
table.createdAt,
|
||||
),
|
||||
}),
|
||||
);
|
||||
@@ -0,0 +1,56 @@
|
||||
import { pgTable, uuid, text, timestamp, jsonb, index } from "drizzle-orm/pg-core";
|
||||
import { companies } from "./companies.js";
|
||||
import { agents } from "./agents.js";
|
||||
import { projects } from "./projects.js";
|
||||
import { issues } from "./issues.js";
|
||||
import { heartbeatRuns } from "./heartbeat_runs.js";
|
||||
import { memoryBindings } from "./memory_bindings.js";
|
||||
import { memoryOperations } from "./memory_operations.js";
|
||||
import type { MemorySourceKind } from "@paperclipai/shared";
|
||||
|
||||
export const memoryLocalRecords = pgTable(
|
||||
"memory_local_records",
|
||||
{
|
||||
id: uuid("id").primaryKey().defaultRandom(),
|
||||
companyId: uuid("company_id").notNull().references(() => companies.id, { onDelete: "cascade" }),
|
||||
bindingId: uuid("binding_id").notNull().references(() => memoryBindings.id, { onDelete: "cascade" }),
|
||||
providerKey: text("provider_key").notNull(),
|
||||
scopeAgentId: uuid("scope_agent_id").references(() => agents.id),
|
||||
scopeProjectId: uuid("scope_project_id").references(() => projects.id),
|
||||
scopeIssueId: uuid("scope_issue_id").references(() => issues.id),
|
||||
scopeRunId: uuid("scope_run_id").references(() => heartbeatRuns.id),
|
||||
scopeSubjectId: text("scope_subject_id"),
|
||||
sourceKind: text("source_kind").$type<MemorySourceKind>(),
|
||||
sourceIssueId: uuid("source_issue_id").references(() => issues.id),
|
||||
sourceCommentId: uuid("source_comment_id"),
|
||||
sourceDocumentKey: text("source_document_key"),
|
||||
sourceRunId: uuid("source_run_id").references(() => heartbeatRuns.id),
|
||||
sourceActivityId: uuid("source_activity_id"),
|
||||
sourceExternalRef: text("source_external_ref"),
|
||||
title: text("title"),
|
||||
content: text("content").notNull(),
|
||||
summary: text("summary"),
|
||||
metadata: jsonb("metadata").$type<Record<string, unknown>>().notNull().default({}),
|
||||
createdByOperationId: uuid("created_by_operation_id").references(() => memoryOperations.id, { onDelete: "set null" }),
|
||||
deletedAt: timestamp("deleted_at", { withTimezone: true }),
|
||||
createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
|
||||
updatedAt: timestamp("updated_at", { withTimezone: true }).notNull().defaultNow(),
|
||||
},
|
||||
(table) => ({
|
||||
companyBindingCreatedIdx: index("memory_local_records_company_binding_created_idx").on(
|
||||
table.companyId,
|
||||
table.bindingId,
|
||||
table.createdAt,
|
||||
),
|
||||
companyAgentCreatedIdx: index("memory_local_records_company_agent_created_idx").on(
|
||||
table.companyId,
|
||||
table.scopeAgentId,
|
||||
table.createdAt,
|
||||
),
|
||||
companyIssueCreatedIdx: index("memory_local_records_company_issue_created_idx").on(
|
||||
table.companyId,
|
||||
table.scopeIssueId,
|
||||
table.createdAt,
|
||||
),
|
||||
}),
|
||||
);
|
||||
@@ -0,0 +1,76 @@
|
||||
import { pgTable, uuid, text, timestamp, integer, jsonb, index } from "drizzle-orm/pg-core";
|
||||
import { companies } from "./companies.js";
|
||||
import { agents } from "./agents.js";
|
||||
import { projects } from "./projects.js";
|
||||
import { issues } from "./issues.js";
|
||||
import { heartbeatRuns } from "./heartbeat_runs.js";
|
||||
import { issueComments } from "./issue_comments.js";
|
||||
import { activityLog } from "./activity_log.js";
|
||||
import { costEvents } from "./cost_events.js";
|
||||
import { financeEvents } from "./finance_events.js";
|
||||
import { memoryBindings } from "./memory_bindings.js";
|
||||
import type {
|
||||
MemoryHookKind,
|
||||
MemoryOperationStatus,
|
||||
MemoryOperationType,
|
||||
MemorySourceKind,
|
||||
MemoryTriggerKind,
|
||||
} from "@paperclipai/shared";
|
||||
|
||||
export const memoryOperations = pgTable(
|
||||
"memory_operations",
|
||||
{
|
||||
id: uuid("id").primaryKey().defaultRandom(),
|
||||
companyId: uuid("company_id").notNull().references(() => companies.id, { onDelete: "cascade" }),
|
||||
bindingId: uuid("binding_id").notNull().references(() => memoryBindings.id, { onDelete: "cascade" }),
|
||||
providerKey: text("provider_key").notNull(),
|
||||
operationType: text("operation_type").$type<MemoryOperationType>().notNull(),
|
||||
triggerKind: text("trigger_kind").$type<MemoryTriggerKind>().notNull().default("manual"),
|
||||
hookKind: text("hook_kind").$type<MemoryHookKind>(),
|
||||
status: text("status").$type<MemoryOperationStatus>().notNull().default("succeeded"),
|
||||
actorType: text("actor_type").notNull(),
|
||||
actorId: text("actor_id").notNull(),
|
||||
agentId: uuid("agent_id").references(() => agents.id),
|
||||
userId: text("user_id"),
|
||||
scopeAgentId: uuid("scope_agent_id").references(() => agents.id),
|
||||
scopeProjectId: uuid("scope_project_id").references(() => projects.id),
|
||||
scopeIssueId: uuid("scope_issue_id").references(() => issues.id),
|
||||
scopeRunId: uuid("scope_run_id").references(() => heartbeatRuns.id),
|
||||
scopeSubjectId: text("scope_subject_id"),
|
||||
sourceKind: text("source_kind").$type<MemorySourceKind>(),
|
||||
sourceIssueId: uuid("source_issue_id").references(() => issues.id),
|
||||
sourceCommentId: uuid("source_comment_id").references(() => issueComments.id),
|
||||
sourceDocumentKey: text("source_document_key"),
|
||||
sourceRunId: uuid("source_run_id").references(() => heartbeatRuns.id),
|
||||
sourceActivityId: uuid("source_activity_id").references(() => activityLog.id),
|
||||
sourceExternalRef: text("source_external_ref"),
|
||||
queryText: text("query_text"),
|
||||
recordCount: integer("record_count").notNull().default(0),
|
||||
requestJson: jsonb("request_json").$type<Record<string, unknown> | null>(),
|
||||
resultJson: jsonb("result_json").$type<Record<string, unknown> | null>(),
|
||||
usageJson: jsonb("usage_json").$type<Array<Record<string, unknown>>>().notNull().default([]),
|
||||
error: text("error"),
|
||||
costEventId: uuid("cost_event_id").references(() => costEvents.id),
|
||||
financeEventId: uuid("finance_event_id").references(() => financeEvents.id),
|
||||
occurredAt: timestamp("occurred_at", { withTimezone: true }).notNull().defaultNow(),
|
||||
createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
|
||||
},
|
||||
(table) => ({
|
||||
companyOccurredIdx: index("memory_operations_company_occurred_idx").on(table.companyId, table.occurredAt),
|
||||
companyBindingOccurredIdx: index("memory_operations_company_binding_occurred_idx").on(
|
||||
table.companyId,
|
||||
table.bindingId,
|
||||
table.occurredAt,
|
||||
),
|
||||
companyIssueOccurredIdx: index("memory_operations_company_issue_occurred_idx").on(
|
||||
table.companyId,
|
||||
table.scopeIssueId,
|
||||
table.occurredAt,
|
||||
),
|
||||
companyRunOccurredIdx: index("memory_operations_company_run_occurred_idx").on(
|
||||
table.companyId,
|
||||
table.scopeRunId,
|
||||
table.occurredAt,
|
||||
),
|
||||
}),
|
||||
);
|
||||
@@ -14,8 +14,21 @@ import type {
|
||||
PluginStateScopeKind,
|
||||
PluginEventType,
|
||||
PluginToolDeclaration,
|
||||
MemoryBinding,
|
||||
MemoryBindingTarget,
|
||||
MemoryCaptureResult,
|
||||
PluginLauncherDeclaration,
|
||||
Company,
|
||||
MemoryForgetResult,
|
||||
MemoryOperation,
|
||||
MemoryProviderCapabilities,
|
||||
MemoryProviderDescriptor,
|
||||
MemoryQueryResult,
|
||||
MemoryRecord,
|
||||
MemoryResolvedBinding,
|
||||
MemoryScope,
|
||||
MemorySourceRef,
|
||||
MemoryUsage,
|
||||
Project,
|
||||
Issue,
|
||||
IssueComment,
|
||||
@@ -34,6 +47,7 @@ export type {
|
||||
PluginJobDeclaration,
|
||||
PluginWebhookDeclaration,
|
||||
PluginToolDeclaration,
|
||||
PluginMemoryProviderDeclaration,
|
||||
PluginUiSlotDeclaration,
|
||||
PluginUiDeclaration,
|
||||
PluginLauncherActionDeclaration,
|
||||
@@ -60,6 +74,19 @@ export type {
|
||||
PluginEventType,
|
||||
PluginBridgeErrorCode,
|
||||
Company,
|
||||
MemoryBinding,
|
||||
MemoryBindingTarget,
|
||||
MemoryCaptureResult,
|
||||
MemoryForgetResult,
|
||||
MemoryOperation,
|
||||
MemoryProviderCapabilities,
|
||||
MemoryProviderDescriptor,
|
||||
MemoryQueryResult,
|
||||
MemoryRecord,
|
||||
MemoryResolvedBinding,
|
||||
MemoryScope,
|
||||
MemorySourceRef,
|
||||
MemoryUsage,
|
||||
Project,
|
||||
Issue,
|
||||
IssueComment,
|
||||
@@ -145,6 +172,30 @@ export interface PluginEvent<TPayload = unknown> {
|
||||
payload: TPayload;
|
||||
}
|
||||
|
||||
export interface PluginMemoryProvider {
|
||||
key: string;
|
||||
displayName: string;
|
||||
description?: string;
|
||||
capabilities?: Partial<MemoryProviderCapabilities>;
|
||||
query?(input: {
|
||||
scope: MemoryScope;
|
||||
query: string;
|
||||
topK?: number;
|
||||
metadataFilter?: Record<string, unknown>;
|
||||
}): Promise<MemoryQueryResult>;
|
||||
capture?(input: {
|
||||
scope: MemoryScope;
|
||||
source: MemorySourceRef;
|
||||
title?: string | null;
|
||||
content: string;
|
||||
summary?: string | null;
|
||||
metadata?: Record<string, unknown>;
|
||||
}): Promise<MemoryCaptureResult>;
|
||||
list?(input: { scope: MemoryScope; limit?: number }): Promise<MemoryRecord[]>;
|
||||
get?(input: { scope: MemoryScope; recordId: string }): Promise<MemoryRecord | null>;
|
||||
forget?(input: { scope: MemoryScope; recordIds: string[] }): Promise<MemoryForgetResult>;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Job context
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
@@ -387,6 +387,49 @@ export const PERMISSION_KEYS = [
|
||||
] as const;
|
||||
export type PermissionKey = (typeof PERMISSION_KEYS)[number];
|
||||
|
||||
export const MEMORY_BINDING_TARGET_TYPES = ["company", "agent"] as const;
|
||||
export type MemoryBindingTargetType = (typeof MEMORY_BINDING_TARGET_TYPES)[number];
|
||||
|
||||
export const MEMORY_PROVIDER_KINDS = ["builtin", "plugin"] as const;
|
||||
export type MemoryProviderKind = (typeof MEMORY_PROVIDER_KINDS)[number];
|
||||
|
||||
export const MEMORY_OPERATION_TYPES = ["query", "capture", "upsert", "list", "get", "forget"] as const;
|
||||
export type MemoryOperationType = (typeof MEMORY_OPERATION_TYPES)[number];
|
||||
|
||||
export const MEMORY_OPERATION_STATUSES = ["succeeded", "failed"] as const;
|
||||
export type MemoryOperationStatus = (typeof MEMORY_OPERATION_STATUSES)[number];
|
||||
|
||||
export const MEMORY_TRIGGER_KINDS = ["manual", "hook"] as const;
|
||||
export type MemoryTriggerKind = (typeof MEMORY_TRIGGER_KINDS)[number];
|
||||
|
||||
export const MEMORY_HOOK_KINDS = [
|
||||
"pre_run_hydrate",
|
||||
"post_run_capture",
|
||||
"issue_comment_capture",
|
||||
"issue_document_capture",
|
||||
] as const;
|
||||
export type MemoryHookKind = (typeof MEMORY_HOOK_KINDS)[number];
|
||||
|
||||
export const MEMORY_SOURCE_KINDS = [
|
||||
"issue_comment",
|
||||
"issue_document",
|
||||
"issue",
|
||||
"run",
|
||||
"activity",
|
||||
"manual_note",
|
||||
"external_document",
|
||||
] as const;
|
||||
export type MemorySourceKind = (typeof MEMORY_SOURCE_KINDS)[number];
|
||||
|
||||
export const MEMORY_EXTRACTION_JOB_STATUSES = [
|
||||
"queued",
|
||||
"running",
|
||||
"succeeded",
|
||||
"failed",
|
||||
"cancelled",
|
||||
] as const;
|
||||
export type MemoryExtractionJobStatus = (typeof MEMORY_EXTRACTION_JOB_STATUSES)[number];
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Plugin System — see doc/plugins/PLUGIN_SPEC.md for the full specification
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -474,6 +517,7 @@ export const PLUGIN_CAPABILITIES = [
|
||||
"activity.log.write",
|
||||
"metrics.write",
|
||||
"telemetry.track",
|
||||
"memory.providers.register",
|
||||
// Plugin State
|
||||
"plugin.state.read",
|
||||
"plugin.state.write",
|
||||
|
||||
@@ -60,6 +60,14 @@ export {
|
||||
JOIN_REQUEST_TYPES,
|
||||
JOIN_REQUEST_STATUSES,
|
||||
PERMISSION_KEYS,
|
||||
MEMORY_BINDING_TARGET_TYPES,
|
||||
MEMORY_PROVIDER_KINDS,
|
||||
MEMORY_OPERATION_TYPES,
|
||||
MEMORY_OPERATION_STATUSES,
|
||||
MEMORY_TRIGGER_KINDS,
|
||||
MEMORY_HOOK_KINDS,
|
||||
MEMORY_SOURCE_KINDS,
|
||||
MEMORY_EXTRACTION_JOB_STATUSES,
|
||||
PLUGIN_API_VERSION,
|
||||
PLUGIN_STATUSES,
|
||||
PLUGIN_CATEGORIES,
|
||||
@@ -133,6 +141,14 @@ export {
|
||||
type JoinRequestType,
|
||||
type JoinRequestStatus,
|
||||
type PermissionKey,
|
||||
type MemoryBindingTargetType,
|
||||
type MemoryProviderKind,
|
||||
type MemoryOperationType,
|
||||
type MemoryOperationStatus,
|
||||
type MemoryTriggerKind,
|
||||
type MemoryHookKind,
|
||||
type MemorySourceKind,
|
||||
type MemoryExtractionJobStatus,
|
||||
type PluginStatus,
|
||||
type PluginCategory,
|
||||
type PluginCapability,
|
||||
@@ -271,6 +287,21 @@ export type {
|
||||
Goal,
|
||||
Approval,
|
||||
ApprovalComment,
|
||||
MemoryProviderCapabilities,
|
||||
MemoryProviderDescriptor,
|
||||
MemoryUsage,
|
||||
MemoryScope,
|
||||
MemorySourceRef,
|
||||
MemoryBinding,
|
||||
MemoryBindingTarget,
|
||||
MemoryResolvedBinding,
|
||||
MemoryRecordHandle,
|
||||
MemoryRecord,
|
||||
MemoryOperation,
|
||||
MemoryExtractionJob,
|
||||
MemoryQueryResult,
|
||||
MemoryCaptureResult,
|
||||
MemoryForgetResult,
|
||||
BudgetPolicy,
|
||||
BudgetPolicySummary,
|
||||
BudgetIncident,
|
||||
@@ -354,6 +385,7 @@ export type {
|
||||
PluginJobDeclaration,
|
||||
PluginWebhookDeclaration,
|
||||
PluginToolDeclaration,
|
||||
PluginMemoryProviderDeclaration,
|
||||
PluginUiSlotDeclaration,
|
||||
PluginLauncherActionDeclaration,
|
||||
PluginLauncherRenderDeclaration,
|
||||
@@ -541,6 +573,20 @@ export {
|
||||
createCostEventSchema,
|
||||
createFinanceEventSchema,
|
||||
updateBudgetSchema,
|
||||
memoryScopeSchema,
|
||||
memorySourceRefSchema,
|
||||
memoryProviderCapabilitiesSchema,
|
||||
createMemoryBindingSchema,
|
||||
updateMemoryBindingSchema,
|
||||
setCompanyMemoryBindingSchema,
|
||||
setAgentMemoryBindingSchema,
|
||||
memoryQuerySchema,
|
||||
memoryCaptureSchema,
|
||||
memoryForgetSchema,
|
||||
memoryListRecordsQuerySchema,
|
||||
memoryListOperationsQuerySchema,
|
||||
memoryListExtractionJobsQuerySchema,
|
||||
memoryBindingTargetTypeSchema,
|
||||
createAssetImageMetadataSchema,
|
||||
createCompanyInviteSchema,
|
||||
createOpenClawInvitePromptSchema,
|
||||
@@ -555,6 +601,18 @@ export {
|
||||
type CreateCostEvent,
|
||||
type CreateFinanceEvent,
|
||||
type UpdateBudget,
|
||||
type MemoryScopeInput,
|
||||
type MemorySourceRefInput,
|
||||
type CreateMemoryBinding,
|
||||
type UpdateMemoryBinding,
|
||||
type SetCompanyMemoryBinding,
|
||||
type SetAgentMemoryBinding,
|
||||
type MemoryQuery,
|
||||
type MemoryCapture,
|
||||
type MemoryForget,
|
||||
type MemoryListRecordsQuery,
|
||||
type MemoryListOperationsQuery,
|
||||
type MemoryListExtractionJobsQuery,
|
||||
type CreateAssetImageMetadata,
|
||||
type CreateCompanyInvite,
|
||||
type CreateOpenClawInvitePrompt,
|
||||
@@ -604,6 +662,7 @@ export {
|
||||
pluginJobDeclarationSchema,
|
||||
pluginWebhookDeclarationSchema,
|
||||
pluginToolDeclarationSchema,
|
||||
pluginMemoryProviderDeclarationSchema,
|
||||
pluginUiSlotDeclarationSchema,
|
||||
pluginLauncherActionDeclarationSchema,
|
||||
pluginLauncherRenderDeclarationSchema,
|
||||
@@ -620,6 +679,7 @@ export {
|
||||
type PluginJobDeclarationInput,
|
||||
type PluginWebhookDeclarationInput,
|
||||
type PluginToolDeclarationInput,
|
||||
type PluginMemoryProviderDeclarationInput,
|
||||
type PluginUiSlotDeclarationInput,
|
||||
type PluginLauncherActionDeclarationInput,
|
||||
type PluginLauncherRenderDeclarationInput,
|
||||
|
||||
@@ -119,6 +119,23 @@ export type {
|
||||
} from "./issue.js";
|
||||
export type { Goal } from "./goal.js";
|
||||
export type { Approval, ApprovalComment } from "./approval.js";
|
||||
export type {
|
||||
MemoryProviderCapabilities,
|
||||
MemoryProviderDescriptor,
|
||||
MemoryUsage,
|
||||
MemoryScope,
|
||||
MemorySourceRef,
|
||||
MemoryBinding,
|
||||
MemoryBindingTarget,
|
||||
MemoryResolvedBinding,
|
||||
MemoryRecordHandle,
|
||||
MemoryRecord,
|
||||
MemoryOperation,
|
||||
MemoryExtractionJob,
|
||||
MemoryQueryResult,
|
||||
MemoryCaptureResult,
|
||||
MemoryForgetResult,
|
||||
} from "./memory.js";
|
||||
export type {
|
||||
BudgetPolicy,
|
||||
BudgetPolicySummary,
|
||||
@@ -210,6 +227,7 @@ export type {
|
||||
PluginJobDeclaration,
|
||||
PluginWebhookDeclaration,
|
||||
PluginToolDeclaration,
|
||||
PluginMemoryProviderDeclaration,
|
||||
PluginUiSlotDeclaration,
|
||||
PluginLauncherActionDeclaration,
|
||||
PluginLauncherRenderDeclaration,
|
||||
|
||||
@@ -0,0 +1,167 @@
|
||||
import type {
|
||||
MemoryBindingTargetType,
|
||||
MemoryExtractionJobStatus,
|
||||
MemoryHookKind,
|
||||
MemoryOperationStatus,
|
||||
MemoryOperationType,
|
||||
MemoryProviderKind,
|
||||
MemorySourceKind,
|
||||
MemoryTriggerKind,
|
||||
} from "../constants.js";
|
||||
|
||||
export interface MemoryProviderCapabilities {
|
||||
browse: boolean;
|
||||
correction: boolean;
|
||||
asyncIngestion: boolean;
|
||||
providerManagedExtraction: boolean;
|
||||
}
|
||||
|
||||
export interface MemoryProviderDescriptor {
|
||||
key: string;
|
||||
displayName: string;
|
||||
description: string | null;
|
||||
kind: MemoryProviderKind;
|
||||
pluginId: string | null;
|
||||
capabilities: MemoryProviderCapabilities;
|
||||
configSchema: Record<string, unknown> | null;
|
||||
}
|
||||
|
||||
export interface MemoryUsage {
|
||||
provider: string;
|
||||
model: string | null;
|
||||
inputTokens: number;
|
||||
outputTokens: number;
|
||||
embeddingTokens: number;
|
||||
costCents: number;
|
||||
latencyMs: number | null;
|
||||
details: Record<string, unknown> | null;
|
||||
}
|
||||
|
||||
export interface MemoryScope {
|
||||
agentId?: string | null;
|
||||
projectId?: string | null;
|
||||
issueId?: string | null;
|
||||
runId?: string | null;
|
||||
subjectId?: string | null;
|
||||
}
|
||||
|
||||
export interface MemorySourceRef {
|
||||
kind: MemorySourceKind;
|
||||
issueId?: string | null;
|
||||
commentId?: string | null;
|
||||
documentKey?: string | null;
|
||||
runId?: string | null;
|
||||
activityId?: string | null;
|
||||
externalRef?: string | null;
|
||||
}
|
||||
|
||||
export interface MemoryBinding {
|
||||
id: string;
|
||||
companyId: string;
|
||||
key: string;
|
||||
name: string | null;
|
||||
providerKey: string;
|
||||
config: Record<string, unknown>;
|
||||
enabled: boolean;
|
||||
createdAt: Date;
|
||||
updatedAt: Date;
|
||||
}
|
||||
|
||||
export interface MemoryBindingTarget {
|
||||
id: string;
|
||||
companyId: string;
|
||||
bindingId: string;
|
||||
targetType: MemoryBindingTargetType;
|
||||
targetId: string;
|
||||
createdAt: Date;
|
||||
updatedAt: Date;
|
||||
}
|
||||
|
||||
export interface MemoryResolvedBinding {
|
||||
companyId: string;
|
||||
targetType: MemoryBindingTargetType | null;
|
||||
targetId: string | null;
|
||||
binding: MemoryBinding | null;
|
||||
}
|
||||
|
||||
export interface MemoryRecordHandle {
|
||||
providerKey: string;
|
||||
recordId: string;
|
||||
}
|
||||
|
||||
export interface MemoryRecord {
|
||||
id: string;
|
||||
companyId: string;
|
||||
bindingId: string;
|
||||
providerKey: string;
|
||||
scope: MemoryScope;
|
||||
source: MemorySourceRef | null;
|
||||
title: string | null;
|
||||
content: string;
|
||||
summary: string | null;
|
||||
metadata: Record<string, unknown>;
|
||||
createdByOperationId: string | null;
|
||||
deletedAt: Date | null;
|
||||
createdAt: Date;
|
||||
updatedAt: Date;
|
||||
}
|
||||
|
||||
export interface MemoryOperation {
|
||||
id: string;
|
||||
companyId: string;
|
||||
bindingId: string;
|
||||
providerKey: string;
|
||||
operationType: MemoryOperationType;
|
||||
triggerKind: MemoryTriggerKind;
|
||||
hookKind: MemoryHookKind | null;
|
||||
status: MemoryOperationStatus;
|
||||
actorType: "agent" | "user" | "system";
|
||||
actorId: string;
|
||||
agentId: string | null;
|
||||
userId: string | null;
|
||||
scope: MemoryScope;
|
||||
source: MemorySourceRef | null;
|
||||
queryText: string | null;
|
||||
recordCount: number;
|
||||
requestJson: Record<string, unknown> | null;
|
||||
resultJson: Record<string, unknown> | null;
|
||||
usage: MemoryUsage[];
|
||||
error: string | null;
|
||||
costEventId: string | null;
|
||||
financeEventId: string | null;
|
||||
occurredAt: Date;
|
||||
createdAt: Date;
|
||||
}
|
||||
|
||||
export interface MemoryExtractionJob {
|
||||
id: string;
|
||||
companyId: string;
|
||||
bindingId: string;
|
||||
providerKey: string;
|
||||
operationId: string | null;
|
||||
status: MemoryExtractionJobStatus;
|
||||
providerJobId: string | null;
|
||||
source: MemorySourceRef | null;
|
||||
resultJson: Record<string, unknown> | null;
|
||||
error: string | null;
|
||||
startedAt: Date | null;
|
||||
completedAt: Date | null;
|
||||
createdAt: Date;
|
||||
updatedAt: Date;
|
||||
}
|
||||
|
||||
export interface MemoryQueryResult {
|
||||
operation: MemoryOperation;
|
||||
records: MemoryRecord[];
|
||||
preamble: string | null;
|
||||
}
|
||||
|
||||
export interface MemoryCaptureResult {
|
||||
operation: MemoryOperation;
|
||||
records: MemoryRecord[];
|
||||
}
|
||||
|
||||
export interface MemoryForgetResult {
|
||||
operation: MemoryOperation;
|
||||
forgottenRecordIds: string[];
|
||||
}
|
||||
@@ -10,6 +10,7 @@ import type {
|
||||
PluginLauncherBounds,
|
||||
PluginLauncherRenderEnvironment,
|
||||
} from "../constants.js";
|
||||
import type { MemoryProviderCapabilities } from "./memory.js";
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// JSON Schema placeholder – plugins declare config schemas as JSON Schema
|
||||
@@ -190,6 +191,19 @@ export interface PluginUiDeclaration {
|
||||
launchers?: PluginLauncherDeclaration[];
|
||||
}
|
||||
|
||||
export interface PluginMemoryProviderDeclaration {
|
||||
/** Stable provider key unique within the plugin manifest. */
|
||||
key: string;
|
||||
/** Human-readable name shown in the operator UI. */
|
||||
displayName: string;
|
||||
/** Optional description of what the provider does. */
|
||||
description?: string;
|
||||
/** Capability flags surfaced in the host provider catalog. */
|
||||
capabilities?: Partial<MemoryProviderCapabilities>;
|
||||
/** Optional JSON Schema for provider-specific binding config. */
|
||||
configSchema?: JsonSchema;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Plugin Manifest V1
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -240,6 +254,8 @@ export interface PaperclipPluginManifestV1 {
|
||||
webhooks?: PluginWebhookDeclaration[];
|
||||
/** Agent tools this plugin contributes. Requires `agent.tools.register` capability. */
|
||||
tools?: PluginToolDeclaration[];
|
||||
/** Memory providers this plugin contributes. Requires `memory.providers.register` capability. */
|
||||
memoryProviders?: PluginMemoryProviderDeclaration[];
|
||||
/**
|
||||
* Legacy top-level launcher declarations.
|
||||
* Prefer `ui.launchers` for new manifests.
|
||||
|
||||
@@ -234,6 +234,35 @@ export {
|
||||
type UpdateBudget,
|
||||
} from "./cost.js";
|
||||
|
||||
export {
|
||||
memoryScopeSchema,
|
||||
memorySourceRefSchema,
|
||||
memoryProviderCapabilitiesSchema,
|
||||
createMemoryBindingSchema,
|
||||
updateMemoryBindingSchema,
|
||||
setCompanyMemoryBindingSchema,
|
||||
setAgentMemoryBindingSchema,
|
||||
memoryQuerySchema,
|
||||
memoryCaptureSchema,
|
||||
memoryForgetSchema,
|
||||
memoryListRecordsQuerySchema,
|
||||
memoryListOperationsQuerySchema,
|
||||
memoryListExtractionJobsQuerySchema,
|
||||
memoryBindingTargetTypeSchema,
|
||||
type MemoryScopeInput,
|
||||
type MemorySourceRefInput,
|
||||
type CreateMemoryBinding,
|
||||
type UpdateMemoryBinding,
|
||||
type SetCompanyMemoryBinding,
|
||||
type SetAgentMemoryBinding,
|
||||
type MemoryQuery,
|
||||
type MemoryCapture,
|
||||
type MemoryForget,
|
||||
type MemoryListRecordsQuery,
|
||||
type MemoryListOperationsQuery,
|
||||
type MemoryListExtractionJobsQuery,
|
||||
} from "./memory.js";
|
||||
|
||||
export {
|
||||
createFinanceEventSchema,
|
||||
type CreateFinanceEvent,
|
||||
@@ -272,6 +301,7 @@ export {
|
||||
pluginJobDeclarationSchema,
|
||||
pluginWebhookDeclarationSchema,
|
||||
pluginToolDeclarationSchema,
|
||||
pluginMemoryProviderDeclarationSchema,
|
||||
pluginUiSlotDeclarationSchema,
|
||||
pluginLauncherActionDeclarationSchema,
|
||||
pluginLauncherRenderDeclarationSchema,
|
||||
@@ -288,6 +318,7 @@ export {
|
||||
type PluginJobDeclarationInput,
|
||||
type PluginWebhookDeclarationInput,
|
||||
type PluginToolDeclarationInput,
|
||||
type PluginMemoryProviderDeclarationInput,
|
||||
type PluginUiSlotDeclarationInput,
|
||||
type PluginLauncherActionDeclarationInput,
|
||||
type PluginLauncherRenderDeclarationInput,
|
||||
|
||||
@@ -0,0 +1,147 @@
|
||||
import { z } from "zod";
|
||||
import {
|
||||
MEMORY_BINDING_TARGET_TYPES,
|
||||
MEMORY_HOOK_KINDS,
|
||||
MEMORY_OPERATION_STATUSES,
|
||||
MEMORY_OPERATION_TYPES,
|
||||
MEMORY_SOURCE_KINDS,
|
||||
} from "../constants.js";
|
||||
|
||||
export const memoryScopeSchema = z
|
||||
.object({
|
||||
agentId: z.string().uuid().nullable().optional(),
|
||||
projectId: z.string().uuid().nullable().optional(),
|
||||
issueId: z.string().uuid().nullable().optional(),
|
||||
runId: z.string().uuid().nullable().optional(),
|
||||
subjectId: z.string().trim().max(200).nullable().optional(),
|
||||
})
|
||||
.strict();
|
||||
|
||||
export const memorySourceRefSchema = z
|
||||
.object({
|
||||
kind: z.enum(MEMORY_SOURCE_KINDS),
|
||||
issueId: z.string().uuid().nullable().optional(),
|
||||
commentId: z.string().uuid().nullable().optional(),
|
||||
documentKey: z.string().trim().max(64).nullable().optional(),
|
||||
runId: z.string().uuid().nullable().optional(),
|
||||
activityId: z.string().uuid().nullable().optional(),
|
||||
externalRef: z.string().trim().max(500).nullable().optional(),
|
||||
})
|
||||
.strict();
|
||||
|
||||
export const memoryProviderCapabilitiesSchema = z
|
||||
.object({
|
||||
browse: z.boolean().optional().default(false),
|
||||
correction: z.boolean().optional().default(false),
|
||||
asyncIngestion: z.boolean().optional().default(false),
|
||||
providerManagedExtraction: z.boolean().optional().default(false),
|
||||
})
|
||||
.strict();
|
||||
|
||||
export const createMemoryBindingSchema = z
|
||||
.object({
|
||||
key: z.string().trim().min(1).max(64).regex(/^[a-z0-9][a-z0-9_-]*$/, "Binding key must be lowercase letters, numbers, _ or -"),
|
||||
name: z.string().trim().max(200).nullable().optional(),
|
||||
providerKey: z.string().trim().min(1).max(128),
|
||||
config: z.record(z.unknown()).optional().default({}),
|
||||
enabled: z.boolean().optional().default(true),
|
||||
})
|
||||
.strict();
|
||||
|
||||
export const updateMemoryBindingSchema = z
|
||||
.object({
|
||||
name: z.string().trim().max(200).nullable().optional(),
|
||||
config: z.record(z.unknown()).optional(),
|
||||
enabled: z.boolean().optional(),
|
||||
})
|
||||
.strict()
|
||||
.refine((value) => Object.keys(value).length > 0, "At least one field must be updated");
|
||||
|
||||
export const setCompanyMemoryBindingSchema = z
|
||||
.object({
|
||||
bindingId: z.string().uuid(),
|
||||
})
|
||||
.strict();
|
||||
|
||||
export const setAgentMemoryBindingSchema = z
|
||||
.object({
|
||||
bindingId: z.string().uuid().nullable(),
|
||||
})
|
||||
.strict();
|
||||
|
||||
export const memoryQuerySchema = z
|
||||
.object({
|
||||
bindingKey: z.string().trim().min(1).max(64).optional(),
|
||||
scope: memoryScopeSchema.optional().default({}),
|
||||
query: z.string().trim().min(1).max(4000),
|
||||
topK: z.number().int().positive().max(25).optional().default(5),
|
||||
intent: z.enum(["agent_preamble", "answer", "browse"]).optional().default("answer"),
|
||||
metadataFilter: z.record(z.unknown()).optional(),
|
||||
})
|
||||
.strict();
|
||||
|
||||
export const memoryCaptureSchema = z
|
||||
.object({
|
||||
bindingKey: z.string().trim().min(1).max(64).optional(),
|
||||
scope: memoryScopeSchema.optional().default({}),
|
||||
source: memorySourceRefSchema,
|
||||
title: z.string().trim().max(200).nullable().optional(),
|
||||
content: z.string().trim().min(1).max(20000),
|
||||
summary: z.string().trim().max(2000).nullable().optional(),
|
||||
metadata: z.record(z.unknown()).optional(),
|
||||
})
|
||||
.strict();
|
||||
|
||||
export const memoryForgetSchema = z
|
||||
.object({
|
||||
recordIds: z.array(z.string().uuid()).min(1).max(100),
|
||||
scope: memoryScopeSchema.optional().default({}),
|
||||
})
|
||||
.strict();
|
||||
|
||||
export const memoryListRecordsQuerySchema = z
|
||||
.object({
|
||||
bindingId: z.string().uuid().optional(),
|
||||
agentId: z.string().uuid().optional(),
|
||||
issueId: z.string().uuid().optional(),
|
||||
runId: z.string().uuid().optional(),
|
||||
sourceKind: z.enum(MEMORY_SOURCE_KINDS).optional(),
|
||||
includeDeleted: z.coerce.boolean().optional().default(false),
|
||||
limit: z.coerce.number().int().positive().max(200).optional().default(50),
|
||||
})
|
||||
.strict();
|
||||
|
||||
export const memoryListOperationsQuerySchema = z
|
||||
.object({
|
||||
bindingId: z.string().uuid().optional(),
|
||||
operationType: z.enum(MEMORY_OPERATION_TYPES).optional(),
|
||||
status: z.enum(MEMORY_OPERATION_STATUSES).optional(),
|
||||
hookKind: z.enum(MEMORY_HOOK_KINDS).optional(),
|
||||
agentId: z.string().uuid().optional(),
|
||||
issueId: z.string().uuid().optional(),
|
||||
runId: z.string().uuid().optional(),
|
||||
limit: z.coerce.number().int().positive().max(200).optional().default(50),
|
||||
})
|
||||
.strict();
|
||||
|
||||
export const memoryListExtractionJobsQuerySchema = z
|
||||
.object({
|
||||
bindingId: z.string().uuid().optional(),
|
||||
limit: z.coerce.number().int().positive().max(200).optional().default(50),
|
||||
})
|
||||
.strict();
|
||||
|
||||
export const memoryBindingTargetTypeSchema = z.enum(MEMORY_BINDING_TARGET_TYPES);
|
||||
|
||||
export type MemoryScopeInput = z.infer<typeof memoryScopeSchema>;
|
||||
export type MemorySourceRefInput = z.infer<typeof memorySourceRefSchema>;
|
||||
export type CreateMemoryBinding = z.infer<typeof createMemoryBindingSchema>;
|
||||
export type UpdateMemoryBinding = z.infer<typeof updateMemoryBindingSchema>;
|
||||
export type SetCompanyMemoryBinding = z.infer<typeof setCompanyMemoryBindingSchema>;
|
||||
export type SetAgentMemoryBinding = z.infer<typeof setAgentMemoryBindingSchema>;
|
||||
export type MemoryQuery = z.infer<typeof memoryQuerySchema>;
|
||||
export type MemoryCapture = z.infer<typeof memoryCaptureSchema>;
|
||||
export type MemoryForget = z.infer<typeof memoryForgetSchema>;
|
||||
export type MemoryListRecordsQuery = z.infer<typeof memoryListRecordsQuerySchema>;
|
||||
export type MemoryListOperationsQuery = z.infer<typeof memoryListOperationsQuerySchema>;
|
||||
export type MemoryListExtractionJobsQuery = z.infer<typeof memoryListExtractionJobsQuerySchema>;
|
||||
@@ -12,6 +12,7 @@ import {
|
||||
PLUGIN_LAUNCHER_RENDER_ENVIRONMENTS,
|
||||
PLUGIN_STATE_SCOPE_KINDS,
|
||||
} from "../constants.js";
|
||||
import { memoryProviderCapabilitiesSchema } from "./memory.js";
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// JSON Schema placeholder – a permissive validator for JSON Schema objects
|
||||
@@ -105,6 +106,16 @@ export const pluginToolDeclarationSchema = z.object({
|
||||
|
||||
export type PluginToolDeclarationInput = z.infer<typeof pluginToolDeclarationSchema>;
|
||||
|
||||
export const pluginMemoryProviderDeclarationSchema = z.object({
|
||||
key: z.string().min(1),
|
||||
displayName: z.string().min(1),
|
||||
description: z.string().optional(),
|
||||
capabilities: memoryProviderCapabilitiesSchema.partial().optional(),
|
||||
configSchema: jsonSchemaSchema.optional(),
|
||||
});
|
||||
|
||||
export type PluginMemoryProviderDeclarationInput = z.infer<typeof pluginMemoryProviderDeclarationSchema>;
|
||||
|
||||
/**
|
||||
* Validates a {@link PluginUiSlotDeclaration} — a UI extension slot the plugin
|
||||
* fills with a React component. Includes `superRefine` checks for slot-specific
|
||||
@@ -405,6 +416,7 @@ export const pluginManifestV1Schema = z.object({
|
||||
jobs: z.array(pluginJobDeclarationSchema).optional(),
|
||||
webhooks: z.array(pluginWebhookDeclarationSchema).optional(),
|
||||
tools: z.array(pluginToolDeclarationSchema).optional(),
|
||||
memoryProviders: z.array(pluginMemoryProviderDeclarationSchema).optional(),
|
||||
launchers: z.array(pluginLauncherDeclarationSchema).optional(),
|
||||
ui: z.object({
|
||||
slots: z.array(pluginUiSlotDeclarationSchema).min(1).optional(),
|
||||
@@ -452,6 +464,16 @@ export const pluginManifestV1Schema = z.object({
|
||||
}
|
||||
}
|
||||
|
||||
if (manifest.memoryProviders && manifest.memoryProviders.length > 0) {
|
||||
if (!manifest.capabilities.includes("memory.providers.register")) {
|
||||
ctx.addIssue({
|
||||
code: z.ZodIssueCode.custom,
|
||||
message: "Capability 'memory.providers.register' is required when memoryProviders are declared",
|
||||
path: ["capabilities"],
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// jobs require jobs.schedule (PLUGIN_SPEC.md §17)
|
||||
if (manifest.jobs && manifest.jobs.length > 0) {
|
||||
if (!manifest.capabilities.includes("jobs.schedule")) {
|
||||
@@ -517,6 +539,18 @@ export const pluginManifestV1Schema = z.object({
|
||||
}
|
||||
}
|
||||
|
||||
if (manifest.memoryProviders) {
|
||||
const providerKeys = manifest.memoryProviders.map((provider) => provider.key);
|
||||
const duplicates = providerKeys.filter((key, i) => providerKeys.indexOf(key) !== i);
|
||||
if (duplicates.length > 0) {
|
||||
ctx.addIssue({
|
||||
code: z.ZodIssueCode.custom,
|
||||
message: `Duplicate memory provider keys: ${[...new Set(duplicates)].join(", ")}`,
|
||||
path: ["memoryProviders"],
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// UI slot ids must be unique within the plugin (namespaced at runtime)
|
||||
if (manifest.ui) {
|
||||
if (manifest.ui.slots) {
|
||||
|
||||
@@ -53,6 +53,10 @@ vi.mock("../services/index.js", () => ({
|
||||
issueApprovalService: () => ({}),
|
||||
issueService: () => mockIssueService,
|
||||
logActivity: mockLogActivity,
|
||||
memoryService: () => ({
|
||||
captureIssueComment: vi.fn(async () => undefined),
|
||||
captureIssueDocument: vi.fn(async () => undefined),
|
||||
}),
|
||||
projectService: () => ({}),
|
||||
routineService: () => ({
|
||||
syncRunStatusForIssue: vi.fn(async () => undefined),
|
||||
|
||||
@@ -58,6 +58,10 @@ function registerRouteMocks() {
|
||||
issueApprovalService: () => ({}),
|
||||
issueService: () => mockIssueService,
|
||||
logActivity: mockLogActivity,
|
||||
memoryService: () => ({
|
||||
captureIssueComment: vi.fn(async () => undefined),
|
||||
captureIssueDocument: vi.fn(async () => undefined),
|
||||
}),
|
||||
projectService: () => ({}),
|
||||
routineService: () => ({
|
||||
syncRunStatusForIssue: vi.fn(async () => undefined),
|
||||
|
||||
@@ -76,6 +76,10 @@ function registerServiceMocks() {
|
||||
issueApprovalService: () => ({}),
|
||||
issueService: () => mockIssueService,
|
||||
logActivity: mockLogActivity,
|
||||
memoryService: () => ({
|
||||
captureIssueComment: vi.fn(async () => undefined),
|
||||
captureIssueDocument: vi.fn(async () => undefined),
|
||||
}),
|
||||
projectService: () => mockProjectService,
|
||||
routineService: () => ({
|
||||
syncRunStatusForIssue: vi.fn(async () => undefined),
|
||||
|
||||
@@ -77,6 +77,10 @@ vi.mock("../services/index.js", () => ({
|
||||
issueApprovalService: () => ({}),
|
||||
issueService: () => mockIssueService,
|
||||
logActivity: mockLogActivity,
|
||||
memoryService: () => ({
|
||||
captureIssueComment: vi.fn(async () => undefined),
|
||||
captureIssueDocument: vi.fn(async () => undefined),
|
||||
}),
|
||||
projectService: () => ({}),
|
||||
routineService: () => mockRoutineService,
|
||||
workProductService: () => ({}),
|
||||
|
||||
@@ -47,6 +47,10 @@ vi.mock("../services/index.js", () => ({
|
||||
issueApprovalService: () => ({}),
|
||||
issueService: () => mockIssueService,
|
||||
logActivity: vi.fn(async () => undefined),
|
||||
memoryService: () => ({
|
||||
captureIssueComment: vi.fn(async () => undefined),
|
||||
captureIssueDocument: vi.fn(async () => undefined),
|
||||
}),
|
||||
projectService: () => ({
|
||||
getById: vi.fn(),
|
||||
listByIds: vi.fn(async () => []),
|
||||
|
||||
@@ -45,6 +45,10 @@ vi.mock("../services/index.js", () => ({
|
||||
issueApprovalService: () => ({}),
|
||||
issueService: () => mockIssueService,
|
||||
logActivity: mockLogActivity,
|
||||
memoryService: () => ({
|
||||
captureIssueComment: vi.fn(async () => undefined),
|
||||
captureIssueDocument: vi.fn(async () => undefined),
|
||||
}),
|
||||
projectService: () => ({}),
|
||||
routineService: () => ({
|
||||
syncRunStatusForIssue: vi.fn(async () => undefined),
|
||||
@@ -169,7 +173,7 @@ describe("issue document revision routes", () => {
|
||||
|
||||
it("rejects invalid document keys before attempting restore", async () => {
|
||||
const res = await request(createApp())
|
||||
.post(`/api/issues/${issueId}/documents/INVALID KEY/revisions/revision-1/restore`)
|
||||
.post(`/api/issues/${issueId}/documents/INVALID.KEY/revisions/revision-1/restore`)
|
||||
.send({});
|
||||
|
||||
expect(res.status).toBe(400);
|
||||
|
||||
@@ -53,6 +53,10 @@ vi.mock("../services/index.js", () => ({
|
||||
issueApprovalService: () => ({}),
|
||||
issueService: () => mockIssueService,
|
||||
logActivity: vi.fn(async () => undefined),
|
||||
memoryService: () => ({
|
||||
captureIssueComment: vi.fn(async () => undefined),
|
||||
captureIssueDocument: vi.fn(async () => undefined),
|
||||
}),
|
||||
projectService: () => ({}),
|
||||
routineService: () => ({
|
||||
syncRunStatusForIssue: vi.fn(async () => undefined),
|
||||
|
||||
@@ -71,6 +71,10 @@ vi.mock("../services/index.js", () => ({
|
||||
issueApprovalService: () => ({}),
|
||||
issueService: () => mockIssueService,
|
||||
logActivity: mockLogActivity,
|
||||
memoryService: () => ({
|
||||
captureIssueComment: vi.fn(async () => undefined),
|
||||
captureIssueDocument: vi.fn(async () => undefined),
|
||||
}),
|
||||
projectService: () => ({}),
|
||||
routineService: () => mockRoutineService,
|
||||
workProductService: () => ({}),
|
||||
|
||||
@@ -45,6 +45,10 @@ vi.mock("../services/index.js", () => ({
|
||||
issueApprovalService: () => ({}),
|
||||
issueService: () => mockIssueService,
|
||||
logActivity: vi.fn(async () => undefined),
|
||||
memoryService: () => ({
|
||||
captureIssueComment: vi.fn(async () => undefined),
|
||||
captureIssueDocument: vi.fn(async () => undefined),
|
||||
}),
|
||||
projectService: () => ({}),
|
||||
routineService: () => ({
|
||||
syncRunStatusForIssue: vi.fn(async () => undefined),
|
||||
|
||||
@@ -53,6 +53,10 @@ vi.mock("../services/index.js", () => ({
|
||||
issueApprovalService: () => ({}),
|
||||
issueService: () => mockIssueService,
|
||||
logActivity: vi.fn(async () => undefined),
|
||||
memoryService: () => ({
|
||||
captureIssueComment: vi.fn(async () => undefined),
|
||||
captureIssueDocument: vi.fn(async () => undefined),
|
||||
}),
|
||||
projectService: () => ({}),
|
||||
routineService: () => ({
|
||||
syncRunStatusForIssue: vi.fn(async () => undefined),
|
||||
|
||||
@@ -60,6 +60,10 @@ vi.mock("../services/index.js", () => ({
|
||||
issueApprovalService: () => ({}),
|
||||
issueService: () => mockIssueService,
|
||||
logActivity: vi.fn(async () => undefined),
|
||||
memoryService: () => ({
|
||||
captureIssueComment: vi.fn(async () => undefined),
|
||||
captureIssueDocument: vi.fn(async () => undefined),
|
||||
}),
|
||||
projectService: () => mockProjectService,
|
||||
routineService: () => ({
|
||||
syncRunStatusForIssue: vi.fn(async () => undefined),
|
||||
|
||||
@@ -26,6 +26,7 @@ import { dashboardRoutes } from "./routes/dashboard.js";
|
||||
import { sidebarBadgeRoutes } from "./routes/sidebar-badges.js";
|
||||
import { inboxDismissalRoutes } from "./routes/inbox-dismissals.js";
|
||||
import { instanceSettingsRoutes } from "./routes/instance-settings.js";
|
||||
import { memoryRoutes } from "./routes/memory.js";
|
||||
import { llmRoutes } from "./routes/llms.js";
|
||||
import { assetRoutes } from "./routes/assets.js";
|
||||
import { accessRoutes } from "./routes/access.js";
|
||||
@@ -164,6 +165,7 @@ export async function createApp(
|
||||
api.use(approvalRoutes(db));
|
||||
api.use(secretRoutes(db));
|
||||
api.use(costRoutes(db));
|
||||
api.use(memoryRoutes(db));
|
||||
api.use(activityRoutes(db));
|
||||
api.use(dashboardRoutes(db));
|
||||
api.use(sidebarBadgeRoutes(db));
|
||||
|
||||
@@ -39,6 +39,7 @@ import {
|
||||
instanceSettingsService,
|
||||
issueApprovalService,
|
||||
issueService,
|
||||
memoryService,
|
||||
documentService,
|
||||
logActivity,
|
||||
projectService,
|
||||
@@ -280,6 +281,7 @@ export function issueRoutes(
|
||||
const svc = issueService(db);
|
||||
const access = accessService(db);
|
||||
const heartbeat = heartbeatService(db);
|
||||
const memorySvc = memoryService(db);
|
||||
const feedback = feedbackService(db);
|
||||
const instanceSettings = instanceSettingsService(db);
|
||||
const agentsSvc = agentService(db);
|
||||
@@ -859,6 +861,25 @@ export function issueRoutes(
|
||||
},
|
||||
});
|
||||
|
||||
void memorySvc.captureIssueDocument({
|
||||
companyId: issue.companyId,
|
||||
issueId: issue.id,
|
||||
agentId: issue.assigneeAgentId ?? null,
|
||||
projectId: issue.projectId ?? null,
|
||||
key: doc.key,
|
||||
title: doc.title ?? null,
|
||||
body: doc.body,
|
||||
actor: {
|
||||
actorType: actor.actorType,
|
||||
actorId: actor.actorId,
|
||||
agentId: actor.agentId ?? null,
|
||||
userId: actor.actorType === "user" ? actor.actorId : null,
|
||||
runId: actor.runId ?? null,
|
||||
},
|
||||
}).catch((err) => {
|
||||
logger.warn({ err, issueId: issue.id, key: doc.key }, "failed to capture issue document to memory");
|
||||
});
|
||||
|
||||
res.status(result.created ? 201 : 200).json(doc);
|
||||
});
|
||||
|
||||
@@ -926,6 +947,28 @@ export function issueRoutes(
|
||||
},
|
||||
});
|
||||
|
||||
void memorySvc.captureIssueDocument({
|
||||
companyId: issue.companyId,
|
||||
issueId: issue.id,
|
||||
agentId: issue.assigneeAgentId ?? null,
|
||||
projectId: issue.projectId ?? null,
|
||||
key: result.document.key,
|
||||
title: result.document.title ?? null,
|
||||
body: result.document.body,
|
||||
actor: {
|
||||
actorType: actor.actorType,
|
||||
actorId: actor.actorId,
|
||||
agentId: actor.agentId ?? null,
|
||||
userId: actor.actorType === "user" ? actor.actorId : null,
|
||||
runId: actor.runId ?? null,
|
||||
},
|
||||
}).catch((err) => {
|
||||
logger.warn(
|
||||
{ err, issueId: issue.id, key: result.document.key },
|
||||
"failed to capture restored issue document to memory",
|
||||
);
|
||||
});
|
||||
|
||||
res.json(result.document);
|
||||
},
|
||||
);
|
||||
@@ -2258,6 +2301,24 @@ export function issueRoutes(
|
||||
},
|
||||
});
|
||||
|
||||
void memorySvc.captureIssueComment({
|
||||
companyId: currentIssue.companyId,
|
||||
issueId: currentIssue.id,
|
||||
commentId: comment.id,
|
||||
agentId: currentIssue.assigneeAgentId ?? null,
|
||||
projectId: currentIssue.projectId ?? null,
|
||||
body: comment.body,
|
||||
actor: {
|
||||
actorType: actor.actorType,
|
||||
actorId: actor.actorId,
|
||||
agentId: actor.agentId ?? null,
|
||||
userId: actor.actorType === "user" ? actor.actorId : null,
|
||||
runId: actor.runId ?? null,
|
||||
},
|
||||
}).catch((err) => {
|
||||
logger.warn({ err, issueId: currentIssue.id, commentId: comment.id }, "failed to capture issue comment to memory");
|
||||
});
|
||||
|
||||
// Merge all wakeups from this comment into one enqueue per agent to avoid duplicate runs.
|
||||
void (async () => {
|
||||
const wakeups = new Map<string, Parameters<typeof heartbeat.wakeup>[1]>();
|
||||
|
||||
@@ -0,0 +1,251 @@
|
||||
import { Router } from "express";
|
||||
import type { Db } from "@paperclipai/db";
|
||||
import {
|
||||
memoryCaptureSchema,
|
||||
memoryForgetSchema,
|
||||
memoryListExtractionJobsQuerySchema,
|
||||
memoryListOperationsQuerySchema,
|
||||
memoryListRecordsQuerySchema,
|
||||
memoryQuerySchema,
|
||||
setAgentMemoryBindingSchema,
|
||||
setCompanyMemoryBindingSchema,
|
||||
updateMemoryBindingSchema,
|
||||
createMemoryBindingSchema,
|
||||
} from "@paperclipai/shared";
|
||||
import { validate } from "../middleware/validate.js";
|
||||
import { forbidden, notFound } from "../errors.js";
|
||||
import { agentService, logActivity, memoryService } from "../services/index.js";
|
||||
import { assertBoard, assertCompanyAccess, getActorInfo } from "./authz.js";
|
||||
|
||||
function actorInfoFromReq(req: any) {
|
||||
if (req.actor.type === "agent") {
|
||||
return {
|
||||
actorType: "agent" as const,
|
||||
actorId: req.actor.agentId ?? "unknown-agent",
|
||||
agentId: req.actor.agentId ?? null,
|
||||
userId: null,
|
||||
runId: req.actor.runId ?? null,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
actorType: req.actor.type === "board" ? ("user" as const) : ("system" as const),
|
||||
actorId: req.actor.type === "board" ? (req.actor.userId ?? "board") : "system",
|
||||
agentId: null,
|
||||
userId: req.actor.type === "board" ? (req.actor.userId ?? null) : null,
|
||||
runId: req.actor.runId ?? null,
|
||||
};
|
||||
}
|
||||
|
||||
export function memoryRoutes(db: Db) {
|
||||
const router = Router();
|
||||
const memory = memoryService(db);
|
||||
const agentsSvc = agentService(db);
|
||||
|
||||
router.get("/companies/:companyId/memory/providers", async (req, res) => {
|
||||
const companyId = req.params.companyId as string;
|
||||
assertCompanyAccess(req, companyId);
|
||||
res.json(await memory.providers());
|
||||
});
|
||||
|
||||
router.get("/companies/:companyId/memory/bindings", async (req, res) => {
|
||||
const companyId = req.params.companyId as string;
|
||||
assertCompanyAccess(req, companyId);
|
||||
res.json(await memory.listBindings(companyId));
|
||||
});
|
||||
|
||||
router.get("/companies/:companyId/memory/targets", async (req, res) => {
|
||||
const companyId = req.params.companyId as string;
|
||||
assertCompanyAccess(req, companyId);
|
||||
res.json(await memory.listTargets(companyId));
|
||||
});
|
||||
|
||||
router.post("/companies/:companyId/memory/bindings", validate(createMemoryBindingSchema), async (req, res) => {
|
||||
const companyId = req.params.companyId as string;
|
||||
assertCompanyAccess(req, companyId);
|
||||
assertBoard(req);
|
||||
const binding = await memory.createBinding(companyId, req.body);
|
||||
const actor = getActorInfo(req);
|
||||
await logActivity(db, {
|
||||
companyId,
|
||||
actorType: actor.actorType,
|
||||
actorId: actor.actorId,
|
||||
agentId: actor.agentId,
|
||||
action: "memory.binding_created",
|
||||
entityType: "memory_binding",
|
||||
entityId: binding.id,
|
||||
details: {
|
||||
key: binding.key,
|
||||
providerKey: binding.providerKey,
|
||||
enabled: binding.enabled,
|
||||
configKeys: Object.keys(binding.config ?? {}).sort(),
|
||||
},
|
||||
});
|
||||
res.status(201).json(binding);
|
||||
});
|
||||
|
||||
router.patch("/memory/bindings/:bindingId", validate(updateMemoryBindingSchema), async (req, res) => {
|
||||
assertBoard(req);
|
||||
const bindingId = req.params.bindingId as string;
|
||||
const binding = await memory.updateBinding(bindingId, req.body);
|
||||
const actor = getActorInfo(req);
|
||||
await logActivity(db, {
|
||||
companyId: binding.companyId,
|
||||
actorType: actor.actorType,
|
||||
actorId: actor.actorId,
|
||||
agentId: actor.agentId,
|
||||
action: "memory.binding_updated",
|
||||
entityType: "memory_binding",
|
||||
entityId: binding.id,
|
||||
details: {
|
||||
changedKeys: Object.keys(req.body as Record<string, unknown>).sort(),
|
||||
enabled: binding.enabled,
|
||||
configKeys: Object.keys(binding.config ?? {}).sort(),
|
||||
},
|
||||
});
|
||||
res.json(binding);
|
||||
});
|
||||
|
||||
router.put("/companies/:companyId/memory/default-binding", validate(setCompanyMemoryBindingSchema), async (req, res) => {
|
||||
const companyId = req.params.companyId as string;
|
||||
assertCompanyAccess(req, companyId);
|
||||
assertBoard(req);
|
||||
const target = await memory.setCompanyDefault(companyId, req.body.bindingId);
|
||||
const actor = getActorInfo(req);
|
||||
await logActivity(db, {
|
||||
companyId,
|
||||
actorType: actor.actorType,
|
||||
actorId: actor.actorId,
|
||||
agentId: actor.agentId,
|
||||
action: "memory.default_set",
|
||||
entityType: "memory_binding",
|
||||
entityId: target.bindingId,
|
||||
details: {
|
||||
targetType: target.targetType,
|
||||
targetId: target.targetId,
|
||||
},
|
||||
});
|
||||
res.json(target);
|
||||
});
|
||||
|
||||
router.get("/agents/:agentId/memory-binding", async (req, res) => {
|
||||
const agentId = req.params.agentId as string;
|
||||
const agent = await agentsSvc.getById(agentId);
|
||||
if (!agent) throw notFound("Agent not found");
|
||||
assertCompanyAccess(req, agent.companyId);
|
||||
res.json(await memory.resolveBinding(agent.companyId, { agentId: agent.id }));
|
||||
});
|
||||
|
||||
router.put("/agents/:agentId/memory-binding", validate(setAgentMemoryBindingSchema), async (req, res) => {
|
||||
const agentId = req.params.agentId as string;
|
||||
const agent = await agentsSvc.getById(agentId);
|
||||
if (!agent) throw notFound("Agent not found");
|
||||
assertCompanyAccess(req, agent.companyId);
|
||||
assertBoard(req);
|
||||
const target = await memory.setAgentOverride(agent.id, req.body.bindingId);
|
||||
const actor = getActorInfo(req);
|
||||
await logActivity(db, {
|
||||
companyId: agent.companyId,
|
||||
actorType: actor.actorType,
|
||||
actorId: actor.actorId,
|
||||
agentId: actor.agentId,
|
||||
action: target ? "memory.agent_override_set" : "memory.agent_override_cleared",
|
||||
entityType: "agent",
|
||||
entityId: agent.id,
|
||||
details: {
|
||||
bindingId: target?.bindingId ?? null,
|
||||
},
|
||||
});
|
||||
res.json(target);
|
||||
});
|
||||
|
||||
router.post("/companies/:companyId/memory/query", validate(memoryQuerySchema), async (req, res) => {
|
||||
const companyId = req.params.companyId as string;
|
||||
assertCompanyAccess(req, companyId);
|
||||
const payload = req.body;
|
||||
if (req.actor.type === "agent" && payload.scope?.agentId && payload.scope.agentId !== req.actor.agentId) {
|
||||
throw forbidden("Agent cannot query memory for another agent scope");
|
||||
}
|
||||
res.json(await memory.query(companyId, payload, actorInfoFromReq(req)));
|
||||
});
|
||||
|
||||
router.post("/companies/:companyId/memory/capture", validate(memoryCaptureSchema), async (req, res) => {
|
||||
const companyId = req.params.companyId as string;
|
||||
assertCompanyAccess(req, companyId);
|
||||
const payload = req.body;
|
||||
if (req.actor.type === "agent" && payload.scope?.agentId && payload.scope.agentId !== req.actor.agentId) {
|
||||
throw forbidden("Agent cannot capture memory for another agent scope");
|
||||
}
|
||||
const result = await memory.capture(companyId, payload, actorInfoFromReq(req));
|
||||
const actor = getActorInfo(req);
|
||||
await logActivity(db, {
|
||||
companyId,
|
||||
actorType: actor.actorType,
|
||||
actorId: actor.actorId,
|
||||
agentId: actor.agentId,
|
||||
action: "memory.captured",
|
||||
entityType: "memory_operation",
|
||||
entityId: result.operation.id,
|
||||
details: {
|
||||
bindingId: result.operation.bindingId,
|
||||
recordIds: result.records.map((record) => record.id),
|
||||
sourceKind: result.operation.source?.kind ?? null,
|
||||
},
|
||||
});
|
||||
res.status(201).json(result);
|
||||
});
|
||||
|
||||
router.post("/companies/:companyId/memory/forget", validate(memoryForgetSchema), async (req, res) => {
|
||||
const companyId = req.params.companyId as string;
|
||||
assertCompanyAccess(req, companyId);
|
||||
assertBoard(req);
|
||||
const result = await memory.forget(companyId, req.body, actorInfoFromReq(req));
|
||||
const actor = getActorInfo(req);
|
||||
await logActivity(db, {
|
||||
companyId,
|
||||
actorType: actor.actorType,
|
||||
actorId: actor.actorId,
|
||||
agentId: actor.agentId,
|
||||
action: "memory.forgotten",
|
||||
entityType: "memory_operation",
|
||||
entityId: result.operation.id,
|
||||
details: {
|
||||
forgottenRecordIds: result.forgottenRecordIds,
|
||||
},
|
||||
});
|
||||
res.json(result);
|
||||
});
|
||||
|
||||
router.get("/companies/:companyId/memory/records", async (req, res) => {
|
||||
const companyId = req.params.companyId as string;
|
||||
assertCompanyAccess(req, companyId);
|
||||
const parsed = memoryListRecordsQuerySchema.parse(req.query);
|
||||
res.json(await memory.listRecords(companyId, parsed));
|
||||
});
|
||||
|
||||
router.get("/companies/:companyId/memory/records/:recordId", async (req, res) => {
|
||||
const companyId = req.params.companyId as string;
|
||||
assertCompanyAccess(req, companyId);
|
||||
const record = await memory.getRecord(companyId, req.params.recordId as string);
|
||||
if (!record) throw notFound("Memory record not found");
|
||||
res.json(record);
|
||||
});
|
||||
|
||||
router.get("/companies/:companyId/memory/operations", async (req, res) => {
|
||||
const companyId = req.params.companyId as string;
|
||||
assertCompanyAccess(req, companyId);
|
||||
assertBoard(req);
|
||||
const parsed = memoryListOperationsQuerySchema.parse(req.query);
|
||||
res.json(await memory.listOperations(companyId, parsed));
|
||||
});
|
||||
|
||||
router.get("/companies/:companyId/memory/extraction-jobs", async (req, res) => {
|
||||
const companyId = req.params.companyId as string;
|
||||
assertCompanyAccess(req, companyId);
|
||||
assertBoard(req);
|
||||
const parsed = memoryListExtractionJobsQuerySchema.parse(req.query);
|
||||
res.json(await memory.listExtractionJobs(companyId, parsed));
|
||||
});
|
||||
|
||||
return router;
|
||||
}
|
||||
@@ -31,6 +31,7 @@ import { getTelemetryClient } from "../telemetry.js";
|
||||
import { companySkillService } from "./company-skills.js";
|
||||
import { budgetService, type BudgetEnforcementScope } from "./budgets.js";
|
||||
import { secretService } from "./secrets.js";
|
||||
import { memoryService } from "./memory.js";
|
||||
import { resolveDefaultAgentWorkspaceDir, resolveManagedProjectWorkspaceDir } from "../home-paths.js";
|
||||
import {
|
||||
buildHeartbeatRunIssueComment,
|
||||
@@ -1190,6 +1191,7 @@ export function heartbeatService(db: Db) {
|
||||
|
||||
const runLogStore = getRunLogStore();
|
||||
const secretsSvc = secretService(db);
|
||||
const memorySvc = memoryService(db);
|
||||
const companySkills = companySkillService(db);
|
||||
const issuesSvc = issueService(db);
|
||||
const executionWorkspacesSvc = executionWorkspaceService(db);
|
||||
@@ -3213,6 +3215,35 @@ export function heartbeatService(db: Db) {
|
||||
);
|
||||
}
|
||||
}
|
||||
if (issueRef) {
|
||||
try {
|
||||
const memoryPreamble = await memorySvc.preRunHydrate({
|
||||
companyId: agent.companyId,
|
||||
agentId: agent.id,
|
||||
projectId: issueRef.projectId ?? null,
|
||||
issueId: issueRef.id,
|
||||
runId: run.id,
|
||||
query: [issueRef.identifier ?? null, issueRef.title]
|
||||
.filter((value): value is string => typeof value === "string" && value.trim().length > 0)
|
||||
.join("\n"),
|
||||
});
|
||||
if (memoryPreamble) {
|
||||
context.paperclipMemoryPreamble = memoryPreamble;
|
||||
await db
|
||||
.update(heartbeatRuns)
|
||||
.set({
|
||||
contextSnapshot: context,
|
||||
updatedAt: new Date(),
|
||||
})
|
||||
.where(eq(heartbeatRuns.id, run.id));
|
||||
}
|
||||
} catch (err) {
|
||||
await onLog(
|
||||
"stderr",
|
||||
`[paperclip] Failed to hydrate memory: ${err instanceof Error ? err.message : String(err)}\n`,
|
||||
);
|
||||
}
|
||||
}
|
||||
const onAdapterMeta = async (meta: AdapterInvocationMeta) => {
|
||||
if (meta.env && secretKeys.size > 0) {
|
||||
for (const key of secretKeys) {
|
||||
@@ -3443,6 +3474,27 @@ export function heartbeatService(db: Db) {
|
||||
);
|
||||
}
|
||||
}
|
||||
const memorySummary =
|
||||
buildHeartbeatRunIssueComment(adapterResult.resultJson ?? null)
|
||||
?? (typeof adapterResult.summary === "string" ? adapterResult.summary.trim() || null : null);
|
||||
if (memorySummary) {
|
||||
try {
|
||||
await memorySvc.captureRunSummary({
|
||||
companyId: agent.companyId,
|
||||
agentId: agent.id,
|
||||
projectId: issueRef?.projectId ?? null,
|
||||
issueId: issueRef?.id ?? null,
|
||||
runId: finalizedRun.id,
|
||||
title: issueRef?.title ? `Run summary: ${issueRef.title}` : "Run summary",
|
||||
summary: memorySummary,
|
||||
});
|
||||
} catch (err) {
|
||||
await onLog(
|
||||
"stderr",
|
||||
`[paperclip] Failed to capture run summary to memory: ${err instanceof Error ? err.message : String(err)}\n`,
|
||||
);
|
||||
}
|
||||
}
|
||||
await finalizeIssueCommentPolicy(finalizedRun, agent);
|
||||
await releaseIssueExecutionAndPromote(finalizedRun);
|
||||
}
|
||||
|
||||
@@ -20,6 +20,7 @@ export { heartbeatService } from "./heartbeat.js";
|
||||
export { dashboardService } from "./dashboard.js";
|
||||
export { sidebarBadgeService } from "./sidebar-badges.js";
|
||||
export { inboxDismissalService } from "./inbox-dismissals.js";
|
||||
export { memoryService } from "./memory.js";
|
||||
export { accessService } from "./access.js";
|
||||
export { boardAuthService } from "./board-auth.js";
|
||||
export { instanceSettingsService } from "./instance-settings.js";
|
||||
|
||||
File diff suppressed because it is too large.
Load diff
@@ -17,3 +17,4 @@ export { instanceSettingsApi } from "./instanceSettings";
|
||||
export { sidebarBadgesApi } from "./sidebarBadges";
|
||||
export { inboxDismissalsApi } from "./inboxDismissals";
|
||||
export { companySkillsApi } from "./companySkills";
|
||||
export { memoryApi } from "./memory";
|
||||
@@ -0,0 +1,52 @@
|
||||
import type {
|
||||
CreateMemoryBinding,
|
||||
MemoryBinding,
|
||||
MemoryBindingTarget,
|
||||
MemoryListOperationsQuery,
|
||||
MemoryListRecordsQuery,
|
||||
MemoryOperation,
|
||||
MemoryProviderDescriptor,
|
||||
MemoryRecord,
|
||||
MemoryResolvedBinding,
|
||||
SetAgentMemoryBinding,
|
||||
SetCompanyMemoryBinding,
|
||||
UpdateMemoryBinding,
|
||||
} from "@paperclipai/shared";
|
||||
import { api } from "./client";
|
||||
|
||||
function buildQueryString(filters?: Record<string, string | number | boolean | undefined>) {
|
||||
const params = new URLSearchParams();
|
||||
for (const [key, value] of Object.entries(filters ?? {})) {
|
||||
if (value === undefined) continue;
|
||||
params.set(key, String(value));
|
||||
}
|
||||
const qs = params.toString();
|
||||
return qs ? `?${qs}` : "";
|
||||
}
|
||||
|
||||
export const memoryApi = {
|
||||
providers: (companyId: string) =>
|
||||
api.get<MemoryProviderDescriptor[]>(`/companies/${encodeURIComponent(companyId)}/memory/providers`),
|
||||
listBindings: (companyId: string) =>
|
||||
api.get<MemoryBinding[]>(`/companies/${encodeURIComponent(companyId)}/memory/bindings`),
|
||||
listTargets: (companyId: string) =>
|
||||
api.get<MemoryBindingTarget[]>(`/companies/${encodeURIComponent(companyId)}/memory/targets`),
|
||||
createBinding: (companyId: string, data: CreateMemoryBinding) =>
|
||||
api.post<MemoryBinding>(`/companies/${encodeURIComponent(companyId)}/memory/bindings`, data),
|
||||
updateBinding: (bindingId: string, data: UpdateMemoryBinding) =>
|
||||
api.patch<MemoryBinding>(`/memory/bindings/${encodeURIComponent(bindingId)}`, data),
|
||||
setCompanyDefault: (companyId: string, bindingId: SetCompanyMemoryBinding["bindingId"]) =>
|
||||
api.put<MemoryBindingTarget>(`/companies/${encodeURIComponent(companyId)}/memory/default-binding`, { bindingId }),
|
||||
getAgentBinding: (agentId: string) =>
|
||||
api.get<MemoryResolvedBinding>(`/agents/${encodeURIComponent(agentId)}/memory-binding`),
|
||||
setAgentBinding: (agentId: string, bindingId: SetAgentMemoryBinding["bindingId"]) =>
|
||||
api.put<MemoryBindingTarget | null>(`/agents/${encodeURIComponent(agentId)}/memory-binding`, { bindingId }),
|
||||
listRecords: (companyId: string, filters?: MemoryListRecordsQuery) =>
|
||||
api.get<MemoryRecord[]>(
|
||||
`/companies/${encodeURIComponent(companyId)}/memory/records${buildQueryString(filters)}`,
|
||||
),
|
||||
listOperations: (companyId: string, filters?: MemoryListOperationsQuery) =>
|
||||
api.get<MemoryOperation[]>(
|
||||
`/companies/${encodeURIComponent(companyId)}/memory/operations${buildQueryString(filters)}`,
|
||||
),
|
||||
};
|
||||
@@ -0,0 +1,252 @@
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
import type { MemoryBinding, MemoryOperation, MemoryRecord } from "@paperclipai/shared";
|
||||
import { memoryApi } from "../api/memory";
|
||||
import { useToast } from "../context/ToastContext";
|
||||
import { queryKeys } from "../lib/queryKeys";
|
||||
import { formatCents, relativeTime } from "../lib/utils";
|
||||
import { Button } from "@/components/ui/button";
|
||||
|
||||
function describeResolvedScope(targetType: "company" | "agent" | null) {
|
||||
if (targetType === "agent") return "Agent override";
|
||||
if (targetType === "company") return "Company default";
|
||||
return "Unconfigured";
|
||||
}
|
||||
|
||||
function describeRecordSource(record: MemoryRecord) {
|
||||
if (!record.source?.kind) return "memory";
|
||||
return record.source.kind.replace(/_/g, " ");
|
||||
}
|
||||
|
||||
function describeBinding(binding: MemoryBinding | null) {
|
||||
if (!binding) return "No memory binding resolves for this agent yet.";
|
||||
return `${binding.name ?? binding.key} (${binding.providerKey})`;
|
||||
}
|
||||
|
||||
function summarizeRecord(record: MemoryRecord) {
|
||||
const body = (record.summary ?? record.content).replace(/\s+/g, " ").trim();
|
||||
return body.length > 280 ? `${body.slice(0, 277)}...` : body;
|
||||
}
|
||||
|
||||
function operationCost(operation: MemoryOperation) {
|
||||
const total = operation.usage.reduce((sum, item) => sum + item.costCents, 0);
|
||||
return total > 0 ? formatCents(total) : "-";
|
||||
}
|
||||
|
||||
export function AgentMemoryTab({
|
||||
companyId,
|
||||
agentId,
|
||||
}: {
|
||||
companyId: string;
|
||||
agentId: string;
|
||||
}) {
|
||||
const { pushToast } = useToast();
|
||||
const queryClient = useQueryClient();
|
||||
const [selectedBindingId, setSelectedBindingId] = useState("__inherit__");
|
||||
|
||||
const bindingsQuery = useQuery({
|
||||
queryKey: queryKeys.memory.bindings(companyId),
|
||||
queryFn: () => memoryApi.listBindings(companyId),
|
||||
});
|
||||
|
||||
const resolvedBindingQuery = useQuery({
|
||||
queryKey: queryKeys.memory.agentBinding(agentId),
|
||||
queryFn: () => memoryApi.getAgentBinding(agentId),
|
||||
});
|
||||
|
||||
const recordsQuery = useQuery({
|
||||
queryKey: queryKeys.memory.records(companyId, { agentId, includeDeleted: false, limit: 20 }),
|
||||
queryFn: () => memoryApi.listRecords(companyId, { agentId, includeDeleted: false, limit: 20 }),
|
||||
});
|
||||
|
||||
const operationsQuery = useQuery({
|
||||
queryKey: queryKeys.memory.operations(companyId, { agentId, limit: 20 }),
|
||||
queryFn: () => memoryApi.listOperations(companyId, { agentId, limit: 20 }),
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
const resolved = resolvedBindingQuery.data;
|
||||
if (!resolved) return;
|
||||
if (resolved.targetType === "agent" && resolved.binding) {
|
||||
setSelectedBindingId(resolved.binding.id);
|
||||
return;
|
||||
}
|
||||
setSelectedBindingId("__inherit__");
|
||||
}, [resolvedBindingQuery.data]);
|
||||
|
||||
const saveOverride = useMutation({
|
||||
mutationFn: () =>
|
||||
memoryApi.setAgentBinding(agentId, selectedBindingId === "__inherit__" ? null : selectedBindingId),
|
||||
onSuccess: async () => {
|
||||
await queryClient.invalidateQueries({ queryKey: queryKeys.memory.all });
|
||||
pushToast({
|
||||
title: "Agent memory binding updated",
|
||||
body: selectedBindingId === "__inherit__"
|
||||
? "This agent now inherits the company default binding."
|
||||
: "The agent override is active for subsequent memory operations.",
|
||||
tone: "success",
|
||||
});
|
||||
},
|
||||
onError: (error) => {
|
||||
pushToast({
|
||||
title: "Failed to update agent memory binding",
|
||||
body: error instanceof Error ? error.message : "Unknown error",
|
||||
tone: "error",
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
const currentSelection = useMemo(() => {
|
||||
const resolved = resolvedBindingQuery.data;
|
||||
if (!resolved) return "__inherit__";
|
||||
if (resolved.targetType === "agent" && resolved.binding) return resolved.binding.id;
|
||||
return "__inherit__";
|
||||
}, [resolvedBindingQuery.data]);
|
||||
|
||||
const loading =
|
||||
bindingsQuery.isLoading
|
||||
|| resolvedBindingQuery.isLoading
|
||||
|| recordsQuery.isLoading
|
||||
|| operationsQuery.isLoading;
|
||||
const error =
|
||||
bindingsQuery.error
|
||||
?? resolvedBindingQuery.error
|
||||
?? recordsQuery.error
|
||||
?? operationsQuery.error
|
||||
?? null;
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<div className="rounded-lg border border-border px-4 py-4">
|
||||
<div className="grid gap-4 lg:grid-cols-[1.4fr_1fr]">
|
||||
<div className="space-y-2">
|
||||
<div className="text-sm font-medium">Resolved memory</div>
|
||||
{loading ? (
|
||||
<p className="text-sm text-muted-foreground">Loading memory state...</p>
|
||||
) : error ? (
|
||||
<p className="text-sm text-destructive">{error.message}</p>
|
||||
) : (
|
||||
<>
|
||||
<div className="text-sm">{describeBinding(resolvedBindingQuery.data?.binding ?? null)}</div>
|
||||
<div className="text-xs text-muted-foreground">
|
||||
Source: {describeResolvedScope(resolvedBindingQuery.data?.targetType ?? null)}
|
||||
</div>
|
||||
<div className="text-xs text-muted-foreground">
|
||||
Automatic capture hooks write recent run summaries and issue context into the resolved binding.
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<div className="text-sm font-medium">Agent override</div>
|
||||
<select
|
||||
value={selectedBindingId}
|
||||
onChange={(event) => setSelectedBindingId(event.target.value)}
|
||||
disabled={loading || Boolean(error)}
|
||||
className="flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 text-sm shadow-xs outline-none"
|
||||
>
|
||||
<option value="__inherit__">Inherit company default</option>
|
||||
{(bindingsQuery.data ?? []).map((binding) => (
|
||||
<option key={binding.id} value={binding.id}>
|
||||
{binding.name ?? binding.key}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
<div className="flex items-center justify-end gap-2">
|
||||
<Button
|
||||
size="sm"
|
||||
disabled={loading || Boolean(error) || saveOverride.isPending || selectedBindingId === currentSelection}
|
||||
onClick={() => saveOverride.mutate()}
|
||||
>
|
||||
{saveOverride.isPending ? "Saving..." : "Save override"}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-3">
|
||||
<div className="flex items-center justify-between">
|
||||
<h3 className="text-sm font-medium">Recent memory records</h3>
|
||||
<div className="text-xs text-muted-foreground">Filtered to this agent scope</div>
|
||||
</div>
|
||||
{loading ? (
|
||||
<p className="text-sm text-muted-foreground">Loading records...</p>
|
||||
) : (recordsQuery.data ?? []).length === 0 ? (
|
||||
<div className="rounded-lg border border-dashed border-border px-4 py-6 text-sm text-muted-foreground">
|
||||
No memory has been captured for this agent yet.
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-3">
|
||||
{(recordsQuery.data ?? []).map((record) => (
|
||||
<div key={record.id} className="rounded-lg border border-border px-4 py-3">
|
||||
<div className="flex flex-col gap-2 md:flex-row md:items-start md:justify-between">
|
||||
<div className="space-y-1">
|
||||
<div className="text-sm font-medium">{record.title ?? describeRecordSource(record)}</div>
|
||||
<div className="text-xs text-muted-foreground">
|
||||
{describeRecordSource(record)} • {relativeTime(record.createdAt)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-xs text-muted-foreground">
|
||||
{record.scope.issueId ? `Issue ${record.scope.issueId.slice(0, 8)}` : "No issue scope"}
|
||||
{record.scope.runId ? ` • Run ${record.scope.runId.slice(0, 8)}` : ""}
|
||||
</div>
|
||||
</div>
|
||||
<p className="mt-2 text-sm text-foreground/90">{summarizeRecord(record)}</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="space-y-3">
|
||||
<div className="flex items-center justify-between">
|
||||
<h3 className="text-sm font-medium">Recent memory operations</h3>
|
||||
<div className="text-xs text-muted-foreground">Queries, captures, and forgets for this agent</div>
|
||||
</div>
|
||||
{loading ? (
|
||||
<p className="text-sm text-muted-foreground">Loading operations...</p>
|
||||
) : (operationsQuery.data ?? []).length === 0 ? (
|
||||
<div className="rounded-lg border border-dashed border-border px-4 py-6 text-sm text-muted-foreground">
|
||||
No memory operations logged yet.
|
||||
</div>
|
||||
) : (
|
||||
<div className="overflow-hidden rounded-lg border border-border">
|
||||
<table className="w-full text-sm">
|
||||
<thead>
|
||||
<tr className="border-b border-border bg-accent/20 text-left text-xs text-muted-foreground">
|
||||
<th className="px-3 py-2 font-medium">Operation</th>
|
||||
<th className="px-3 py-2 font-medium">Hook</th>
|
||||
<th className="px-3 py-2 font-medium">Status</th>
|
||||
<th className="px-3 py-2 font-medium text-right">Records</th>
|
||||
<th className="px-3 py-2 font-medium text-right">Cost</th>
|
||||
<th className="px-3 py-2 font-medium text-right">When</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{(operationsQuery.data ?? []).map((operation) => (
|
||||
<tr key={operation.id} className="border-b border-border last:border-b-0">
|
||||
<td className="px-3 py-2">
|
||||
<div className="font-medium">{operation.operationType}</div>
|
||||
<div className="text-xs text-muted-foreground">{operation.providerKey}</div>
|
||||
</td>
|
||||
<td className="px-3 py-2 text-xs text-muted-foreground">
|
||||
{operation.hookKind ?? operation.triggerKind}
|
||||
</td>
|
||||
<td className="px-3 py-2 text-xs">{operation.status}</td>
|
||||
<td className="px-3 py-2 text-right tabular-nums">{operation.recordCount}</td>
|
||||
<td className="px-3 py-2 text-right tabular-nums">{operationCost(operation)}</td>
|
||||
<td className="px-3 py-2 text-right text-xs text-muted-foreground">
|
||||
{relativeTime(operation.occurredAt)}
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,455 @@
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
import type {
|
||||
MemoryBinding,
|
||||
MemoryProviderDescriptor,
|
||||
} from "@paperclipai/shared";
|
||||
import { memoryApi } from "../api/memory";
|
||||
import { useToast } from "../context/ToastContext";
|
||||
import { queryKeys } from "../lib/queryKeys";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Textarea } from "@/components/ui/textarea";
|
||||
|
||||
const DEFAULT_LOCAL_BASIC_CONFIG = {
|
||||
enablePreRunHydrate: true,
|
||||
enablePostRunCapture: true,
|
||||
enableIssueCommentCapture: false,
|
||||
enableIssueDocumentCapture: true,
|
||||
maxHydrateSnippets: 5,
|
||||
};
|
||||
|
||||
function prettyJson(value: Record<string, unknown>) {
|
||||
return JSON.stringify(value, null, 2);
|
||||
}
|
||||
|
||||
function parseConfigText(configText: string) {
|
||||
let parsed: unknown;
|
||||
try {
|
||||
parsed = JSON.parse(configText);
|
||||
} catch (error) {
|
||||
throw new Error(error instanceof Error ? error.message : "Config must be valid JSON");
|
||||
}
|
||||
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
|
||||
throw new Error("Config must be a JSON object");
|
||||
}
|
||||
return parsed as Record<string, unknown>;
|
||||
}
|
||||
|
||||
function describeBindingConfig(binding: MemoryBinding) {
|
||||
if (binding.providerKey !== "local_basic") {
|
||||
const keys = Object.keys(binding.config ?? {});
|
||||
return keys.length > 0 ? `${keys.length} config field${keys.length === 1 ? "" : "s"}` : "Default config";
|
||||
}
|
||||
|
||||
const config = { ...DEFAULT_LOCAL_BASIC_CONFIG, ...binding.config };
|
||||
return [
|
||||
config.enablePreRunHydrate ? "pre-run hydrate on" : "pre-run hydrate off",
|
||||
config.enablePostRunCapture ? "post-run capture on" : "post-run capture off",
|
||||
config.enableIssueDocumentCapture ? "issue docs on" : "issue docs off",
|
||||
config.enableIssueCommentCapture ? "comments on" : "comments off",
|
||||
`top ${String(config.maxHydrateSnippets)} snippets`,
|
||||
].join(" • ");
|
||||
}
|
||||
|
||||
function providerLabel(provider: MemoryProviderDescriptor | undefined, binding: MemoryBinding) {
|
||||
return provider?.displayName ?? binding.providerKey;
|
||||
}
|
||||
|
||||
function providerDescription(provider: MemoryProviderDescriptor | undefined) {
|
||||
return provider?.description ?? "Memory provider";
|
||||
}
|
||||
|
||||
function MemoryBindingCard({
|
||||
binding,
|
||||
isDefault,
|
||||
overrideCount,
|
||||
provider,
|
||||
onSetDefault,
|
||||
}: {
|
||||
binding: MemoryBinding;
|
||||
isDefault: boolean;
|
||||
overrideCount: number;
|
||||
provider?: MemoryProviderDescriptor;
|
||||
onSetDefault: (bindingId: string) => void;
|
||||
}) {
|
||||
const { pushToast } = useToast();
|
||||
const queryClient = useQueryClient();
|
||||
const [name, setName] = useState(binding.name ?? "");
|
||||
const [enabled, setEnabled] = useState(binding.enabled);
|
||||
const [configText, setConfigText] = useState(prettyJson(binding.config ?? {}));
|
||||
const [configError, setConfigError] = useState<string | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
setName(binding.name ?? "");
|
||||
setEnabled(binding.enabled);
|
||||
setConfigText(prettyJson(binding.config ?? {}));
|
||||
setConfigError(null);
|
||||
}, [binding]);
|
||||
|
||||
const dirty =
|
||||
name !== (binding.name ?? "")
|
||||
|| enabled !== binding.enabled
|
||||
|| configText !== prettyJson(binding.config ?? {});
|
||||
|
||||
const updateBinding = useMutation({
|
||||
mutationFn: async () => {
|
||||
const config = parseConfigText(configText);
|
||||
return memoryApi.updateBinding(binding.id, {
|
||||
name: name.trim() || null,
|
||||
enabled,
|
||||
config,
|
||||
});
|
||||
},
|
||||
onSuccess: async () => {
|
||||
setConfigError(null);
|
||||
await queryClient.invalidateQueries({ queryKey: queryKeys.memory.all });
|
||||
pushToast({
|
||||
title: "Memory binding updated",
|
||||
body: `${binding.key} saved successfully.`,
|
||||
tone: "success",
|
||||
});
|
||||
},
|
||||
onError: (error) => {
|
||||
pushToast({
|
||||
title: "Failed to update memory binding",
|
||||
body: error instanceof Error ? error.message : "Unknown error",
|
||||
tone: "error",
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="rounded-md border border-border px-4 py-4">
|
||||
<div className="flex flex-col gap-3 md:flex-row md:items-start md:justify-between">
|
||||
<div className="space-y-1">
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
<h3 className="text-sm font-medium">{binding.name ?? binding.key}</h3>
|
||||
<span className="rounded-full bg-accent px-2 py-0.5 text-[11px] text-muted-foreground">
|
||||
{providerLabel(provider, binding)}
|
||||
</span>
|
||||
<span
|
||||
className={`rounded-full px-2 py-0.5 text-[11px] ${
|
||||
binding.enabled
|
||||
? "bg-emerald-500/10 text-emerald-700 dark:text-emerald-300"
|
||||
: "bg-amber-500/10 text-amber-700 dark:text-amber-300"
|
||||
}`}
|
||||
>
|
||||
{binding.enabled ? "Enabled" : "Disabled"}
|
||||
</span>
|
||||
{isDefault && (
|
||||
<span className="rounded-full bg-blue-500/10 px-2 py-0.5 text-[11px] text-blue-700 dark:text-blue-300">
|
||||
Company default
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<div className="text-xs text-muted-foreground">Key: {binding.key}</div>
|
||||
<div className="text-xs text-muted-foreground">{providerDescription(provider)}</div>
|
||||
<div className="text-xs text-muted-foreground">{describeBindingConfig(binding)}</div>
|
||||
<div className="text-xs text-muted-foreground">
|
||||
{overrideCount > 0 ? `${overrideCount} agent override${overrideCount === 1 ? "" : "s"}` : "No agent overrides"}
|
||||
</div>
|
||||
</div>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
disabled={isDefault}
|
||||
onClick={() => onSetDefault(binding.id)}
|
||||
>
|
||||
{isDefault ? "Default" : "Set as default"}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div className="mt-4 grid gap-3">
|
||||
<div className="grid gap-3 md:grid-cols-[1fr_auto] md:items-end">
|
||||
<div className="space-y-1">
|
||||
<label className="text-xs font-medium text-muted-foreground">Display name</label>
|
||||
<Input
|
||||
value={name}
|
||||
onChange={(event) => setName(event.target.value)}
|
||||
placeholder="Optional label"
|
||||
/>
|
||||
</div>
|
||||
<label className="flex items-center gap-2 text-sm">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={enabled}
|
||||
onChange={(event) => setEnabled(event.target.checked)}
|
||||
/>
|
||||
Enabled
|
||||
</label>
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
<label className="text-xs font-medium text-muted-foreground">Provider config JSON</label>
|
||||
<Textarea
|
||||
value={configText}
|
||||
onChange={(event) => {
|
||||
setConfigText(event.target.value);
|
||||
setConfigError(null);
|
||||
}}
|
||||
className="min-h-44 font-mono text-xs"
|
||||
/>
|
||||
{configError && <p className="text-xs text-destructive">{configError}</p>}
|
||||
{updateBinding.isError && !configError && (
|
||||
<p className="text-xs text-destructive">
|
||||
{updateBinding.error instanceof Error ? updateBinding.error.message : "Failed to update binding"}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex items-center justify-end gap-2">
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
disabled={!dirty || updateBinding.isPending}
|
||||
onClick={() => {
|
||||
try {
|
||||
parseConfigText(configText);
|
||||
setConfigError(null);
|
||||
updateBinding.mutate();
|
||||
} catch (error) {
|
||||
setConfigError(error instanceof Error ? error.message : "Config must be valid JSON");
|
||||
}
|
||||
}}
|
||||
>
|
||||
{updateBinding.isPending ? "Saving..." : "Save binding"}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function CompanyMemorySettings({ companyId }: { companyId: string }) {
|
||||
const { pushToast } = useToast();
|
||||
const queryClient = useQueryClient();
|
||||
const [key, setKey] = useState("default-memory");
|
||||
const [name, setName] = useState("Default memory");
|
||||
const [providerKey, setProviderKey] = useState("local_basic");
|
||||
const [configText, setConfigText] = useState(prettyJson(DEFAULT_LOCAL_BASIC_CONFIG));
|
||||
const [enabled, setEnabled] = useState(true);
|
||||
const [makeDefault, setMakeDefault] = useState(true);
|
||||
const [createError, setCreateError] = useState<string | null>(null);
|
||||
|
||||
const providersQuery = useQuery({
|
||||
queryKey: queryKeys.memory.providers(companyId),
|
||||
queryFn: () => memoryApi.providers(companyId),
|
||||
});
|
||||
|
||||
const bindingsQuery = useQuery({
|
||||
queryKey: queryKeys.memory.bindings(companyId),
|
||||
queryFn: () => memoryApi.listBindings(companyId),
|
||||
});
|
||||
|
||||
const targetsQuery = useQuery({
|
||||
queryKey: queryKeys.memory.targets(companyId),
|
||||
queryFn: () => memoryApi.listTargets(companyId),
|
||||
});
|
||||
|
||||
const providersByKey = useMemo(
|
||||
() => new Map((providersQuery.data ?? []).map((provider) => [provider.key, provider])),
|
||||
[providersQuery.data],
|
||||
);
|
||||
|
||||
const defaultBindingId =
|
||||
targetsQuery.data?.find((target) => target.targetType === "company" && target.targetId === companyId)?.bindingId ?? null;
|
||||
|
||||
const overrideCountByBindingId = useMemo(() => {
|
||||
const result = new Map<string, number>();
|
||||
for (const target of targetsQuery.data ?? []) {
|
||||
if (target.targetType !== "agent") continue;
|
||||
result.set(target.bindingId, (result.get(target.bindingId) ?? 0) + 1);
|
||||
}
|
||||
return result;
|
||||
}, [targetsQuery.data]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!providersQuery.data?.length) return;
|
||||
if (providersQuery.data.some((provider) => provider.key === providerKey)) return;
|
||||
setProviderKey(providersQuery.data[0]!.key);
|
||||
}, [providerKey, providersQuery.data]);
|
||||
|
||||
const createBinding = useMutation({
|
||||
mutationFn: async () => {
|
||||
const config = parseConfigText(configText);
|
||||
const created = await memoryApi.createBinding(companyId, {
|
||||
key: key.trim(),
|
||||
name: name.trim() || null,
|
||||
providerKey,
|
||||
config,
|
||||
enabled,
|
||||
});
|
||||
if (makeDefault) {
|
||||
await memoryApi.setCompanyDefault(companyId, created.id);
|
||||
}
|
||||
return created;
|
||||
},
|
||||
onSuccess: async () => {
|
||||
setCreateError(null);
|
||||
setKey("default-memory");
|
||||
setName("Default memory");
|
||||
setConfigText(prettyJson(DEFAULT_LOCAL_BASIC_CONFIG));
|
||||
setEnabled(true);
|
||||
setMakeDefault(true);
|
||||
await queryClient.invalidateQueries({ queryKey: queryKeys.memory.all });
|
||||
pushToast({
|
||||
title: "Memory binding created",
|
||||
body: "The new binding is ready for company and agent scopes.",
|
||||
tone: "success",
|
||||
});
|
||||
},
|
||||
onError: (error) => {
|
||||
pushToast({
|
||||
title: "Failed to create memory binding",
|
||||
body: error instanceof Error ? error.message : "Unknown error",
|
||||
tone: "error",
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
const setCompanyDefault = useMutation({
|
||||
mutationFn: (bindingId: string) => memoryApi.setCompanyDefault(companyId, bindingId),
|
||||
onSuccess: async () => {
|
||||
await queryClient.invalidateQueries({ queryKey: queryKeys.memory.all });
|
||||
pushToast({
|
||||
title: "Company default updated",
|
||||
body: "New runs will resolve memory through the selected binding.",
|
||||
tone: "success",
|
||||
});
|
||||
},
|
||||
onError: (error) => {
|
||||
pushToast({
|
||||
title: "Failed to update company memory default",
|
||||
body: error instanceof Error ? error.message : "Unknown error",
|
||||
tone: "error",
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
const isLoading = providersQuery.isLoading || bindingsQuery.isLoading || targetsQuery.isLoading;
|
||||
const error = providersQuery.error ?? bindingsQuery.error ?? targetsQuery.error ?? null;
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<div className="text-xs font-medium uppercase tracking-wide text-muted-foreground">
|
||||
Memory
|
||||
</div>
|
||||
<div className="space-y-4 rounded-md border border-border px-4 py-4">
|
||||
<div className="space-y-1">
|
||||
<h2 className="text-sm font-medium">Company memory bindings</h2>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Bindings determine where agent memory is hydrated from and where run summaries, issue documents, and other captured context are written.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{isLoading ? (
|
||||
<p className="text-sm text-muted-foreground">Loading memory settings...</p>
|
||||
) : error ? (
|
||||
<p className="text-sm text-destructive">{error.message}</p>
|
||||
) : (
|
||||
<>
|
||||
<div className="space-y-3">
|
||||
<div className="grid gap-3 md:grid-cols-2">
|
||||
<div className="space-y-1">
|
||||
<label className="text-xs font-medium text-muted-foreground">Binding key</label>
|
||||
<Input value={key} onChange={(event) => setKey(event.target.value)} placeholder="default-memory" />
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
<label className="text-xs font-medium text-muted-foreground">Display name</label>
|
||||
<Input value={name} onChange={(event) => setName(event.target.value)} placeholder="Default memory" />
|
||||
</div>
|
||||
</div>
|
||||
<div className="grid gap-3 md:grid-cols-[1fr_auto_auto] md:items-end">
|
||||
<div className="space-y-1">
|
||||
<label className="text-xs font-medium text-muted-foreground">Provider</label>
|
||||
<select
|
||||
value={providerKey}
|
||||
onChange={(event) => setProviderKey(event.target.value)}
|
||||
className="flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 text-sm shadow-xs outline-none"
|
||||
>
|
||||
{(providersQuery.data ?? []).map((provider) => (
|
||||
<option key={provider.key} value={provider.key}>
|
||||
{provider.displayName}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
<label className="flex items-center gap-2 text-sm">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={enabled}
|
||||
onChange={(event) => setEnabled(event.target.checked)}
|
||||
/>
|
||||
Enabled
|
||||
</label>
|
||||
<label className="flex items-center gap-2 text-sm">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={makeDefault}
|
||||
onChange={(event) => setMakeDefault(event.target.checked)}
|
||||
/>
|
||||
Set as company default
|
||||
</label>
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
<label className="text-xs font-medium text-muted-foreground">Config JSON</label>
|
||||
<Textarea
|
||||
value={configText}
|
||||
onChange={(event) => {
|
||||
setConfigText(event.target.value);
|
||||
setCreateError(null);
|
||||
}}
|
||||
className="min-h-44 font-mono text-xs"
|
||||
/>
|
||||
{createError && <p className="text-xs text-destructive">{createError}</p>}
|
||||
</div>
|
||||
<div className="flex items-center justify-end gap-2">
|
||||
<Button
|
||||
size="sm"
|
||||
disabled={!key.trim() || !providerKey || createBinding.isPending}
|
||||
onClick={() => {
|
||||
try {
|
||||
parseConfigText(configText);
|
||||
setCreateError(null);
|
||||
createBinding.mutate();
|
||||
} catch (error) {
|
||||
setCreateError(error instanceof Error ? error.message : "Config must be valid JSON");
|
||||
}
|
||||
}}
|
||||
>
|
||||
{createBinding.isPending ? "Creating..." : "Create binding"}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-3">
|
||||
<div className="flex items-center justify-between">
|
||||
<h3 className="text-sm font-medium">Existing bindings</h3>
|
||||
<div className="text-xs text-muted-foreground">
|
||||
{defaultBindingId ? "A company default is configured." : "No company default yet."}
|
||||
</div>
|
||||
</div>
|
||||
{(bindingsQuery.data ?? []).length === 0 ? (
|
||||
<div className="rounded-md border border-dashed border-border px-4 py-6 text-sm text-muted-foreground">
|
||||
Create a binding to enable memory hydration and capture for this company.
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-3">
|
||||
{(bindingsQuery.data ?? []).map((binding) => (
|
||||
<MemoryBindingCard
|
||||
key={binding.id}
|
||||
binding={binding}
|
||||
isDefault={binding.id === defaultBindingId}
|
||||
overrideCount={overrideCountByBindingId.get(binding.id) ?? 0}
|
||||
provider={providersByKey.get(binding.providerKey)}
|
||||
onSetDefault={(bindingId) => setCompanyDefault.mutate(bindingId)}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -28,6 +28,18 @@ export const queryKeys = {
|
||||
detectModel: (companyId: string, adapterType: string) =>
|
||||
["agents", companyId, "detect-model", adapterType] as const,
|
||||
},
|
||||
memory: {
|
||||
all: ["memory"] as const,
|
||||
company: (companyId: string) => ["memory", companyId] as const,
|
||||
providers: (companyId: string) => ["memory", companyId, "providers"] as const,
|
||||
bindings: (companyId: string) => ["memory", companyId, "bindings"] as const,
|
||||
targets: (companyId: string) => ["memory", companyId, "targets"] as const,
|
||||
agentBinding: (agentId: string) => ["memory", "agent-binding", agentId] as const,
|
||||
records: (companyId: string, filters?: Record<string, string | number | boolean | undefined>) =>
|
||||
["memory", companyId, "records", filters ?? {}] as const,
|
||||
operations: (companyId: string, filters?: Record<string, string | number | boolean | undefined>) =>
|
||||
["memory", companyId, "operations", filters ?? {}] as const,
|
||||
},
|
||||
issues: {
|
||||
list: (companyId: string) => ["issues", companyId] as const,
|
||||
search: (companyId: string, q: string, projectId?: string, limit?: number) =>
|
||||
|
||||
@@ -23,6 +23,7 @@ import { useDialog } from "../context/DialogContext";
|
||||
import { useBreadcrumbs } from "../context/BreadcrumbContext";
|
||||
import { queryKeys } from "../lib/queryKeys";
|
||||
import { AgentConfigForm } from "../components/AgentConfigForm";
|
||||
import { AgentMemoryTab } from "../components/AgentMemoryTab";
|
||||
import { PageTabBar } from "../components/PageTabBar";
|
||||
import { adapterLabels, roleLabels, help } from "../components/agent-config-primitives";
|
||||
import { ToggleSwitch } from "@/components/ui/toggle-switch";
|
||||
@@ -223,13 +224,14 @@ function scrollToContainerBottom(container: ScrollContainer, behavior: ScrollBeh
|
||||
container.scrollTo({ top: container.scrollHeight, behavior });
|
||||
}
|
||||
|
||||
type AgentDetailView = "dashboard" | "instructions" | "configuration" | "skills" | "runs" | "budget";
|
||||
type AgentDetailView = "dashboard" | "instructions" | "configuration" | "skills" | "runs" | "budget" | "memory";
|
||||
|
||||
function parseAgentDetailView(value: string | null): AgentDetailView {
|
||||
if (value === "instructions" || value === "prompts") return "instructions";
|
||||
if (value === "configure" || value === "configuration") return "configuration";
|
||||
if (value === "skills") return "skills";
|
||||
if (value === "budget") return "budget";
|
||||
if (value === "memory") return "memory";
|
||||
if (value === "runs") return value;
|
||||
return "dashboard";
|
||||
}
|
||||
@@ -748,6 +750,8 @@ export function AgentDetail() {
|
||||
? "runs"
|
||||
: activeView === "budget"
|
||||
? "budget"
|
||||
: activeView === "memory"
|
||||
? "memory"
|
||||
: "dashboard";
|
||||
if (routeAgentRef !== canonicalAgentRef || urlTab !== canonicalTab) {
|
||||
navigate(`/agents/${canonicalAgentRef}/${canonicalTab}`, { replace: true });
|
||||
@@ -865,6 +869,8 @@ export function AgentDetail() {
|
||||
crumbs.push({ label: "Instructions" });
|
||||
} else if (activeView === "configuration") {
|
||||
crumbs.push({ label: "Configuration" });
|
||||
} else if (activeView === "memory") {
|
||||
crumbs.push({ label: "Memory" });
|
||||
// } else if (activeView === "skills") { // TODO: bring back later
|
||||
// crumbs.push({ label: "Skills" });
|
||||
} else if (activeView === "runs") {
|
||||
@@ -1009,6 +1015,7 @@ export function AgentDetail() {
|
||||
{ value: "instructions", label: "Instructions" },
|
||||
{ value: "skills", label: "Skills" },
|
||||
{ value: "configuration", label: "Configuration" },
|
||||
{ value: "memory", label: "Memory" },
|
||||
{ value: "runs", label: "Runs" },
|
||||
{ value: "budget", label: "Budget" },
|
||||
]}
|
||||
@@ -1136,6 +1143,13 @@ export function AgentDetail() {
|
||||
/>
|
||||
)}
|
||||
|
||||
{activeView === "memory" && resolvedCompanyId ? (
|
||||
<AgentMemoryTab
|
||||
companyId={resolvedCompanyId}
|
||||
agentId={agent.id}
|
||||
/>
|
||||
) : null}
|
||||
|
||||
{activeView === "budget" && resolvedCompanyId ? (
|
||||
<div className="max-w-3xl">
|
||||
<BudgetPolicyCard
|
||||
|
||||
@@ -17,6 +17,7 @@ import {
|
||||
ToggleField,
|
||||
HintIcon
|
||||
} from "../components/agent-config-primitives";
|
||||
import { CompanyMemorySettings } from "../components/CompanyMemorySettings";
|
||||
|
||||
type AgentSnippetInput = {
|
||||
onboardingTextUrl: string;
|
||||
@@ -417,6 +418,8 @@ export function CompanySettings() {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<CompanyMemorySettings companyId={selectedCompanyId!} />
|
||||
|
||||
<div className="space-y-4">
|
||||
<div className="text-xs font-medium text-muted-foreground uppercase tracking-wide">
|
||||
Feedback Sharing
|
||||
|
||||
Reference in new issue
Block a user