Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 25 additions & 3 deletions apps/contracts/openapi.json
Original file line number Diff line number Diff line change
Expand Up @@ -9141,9 +9141,29 @@
"/api/v1/user-memory/consolidation/plans": {
"post": {
"operationId": "generateMemoryConsolidationPlan",
"requestBody": {
"required": false,
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"target": {
"type": "string",
"enum": [
"user_model",
"project_memory"
],
"default": "user_model"
}
}
}
}
}
},
"responses": {
"201": {
"description": "A bounded daemon-curated user-memory consolidation plan.",
"description": "A bounded daemon-curated consolidation plan for the requested memory store.",
"content": {
"application/json": {
"schema": {
Expand Down Expand Up @@ -9248,7 +9268,8 @@
"target": {
"type": "string",
"enum": [
"user_model"
"user_model",
"project_memory"
]
}
},
Expand Down Expand Up @@ -9454,7 +9475,8 @@
"target": {
"type": "string",
"enum": [
"user_model"
"user_model",
"project_memory"
]
}
},
Expand Down
9 changes: 8 additions & 1 deletion apps/contracts/src/generated/sdk.gen.ts
Original file line number Diff line number Diff line change
Expand Up @@ -210,7 +210,14 @@ export const undoLearningPromotion = <ThrowOnError extends boolean = false>(opti

export const reflectSession = <ThrowOnError extends boolean = false>(options: Options<ReflectSessionData, ThrowOnError>): RequestResult<ReflectSessionResponses, ReflectSessionErrors, ThrowOnError> => (options.client ?? client).post<ReflectSessionResponses, ReflectSessionErrors, ThrowOnError>({ url: '/api/v1/sessions/{sessionId}/reflection', ...options });

export const generateMemoryConsolidationPlan = <ThrowOnError extends boolean = false>(options?: Options<GenerateMemoryConsolidationPlanData, ThrowOnError>): RequestResult<GenerateMemoryConsolidationPlanResponses, GenerateMemoryConsolidationPlanErrors, ThrowOnError> => (options?.client ?? client).post<GenerateMemoryConsolidationPlanResponses, GenerateMemoryConsolidationPlanErrors, ThrowOnError>({ url: '/api/v1/user-memory/consolidation/plans', ...options });
export const generateMemoryConsolidationPlan = <ThrowOnError extends boolean = false>(options?: Options<GenerateMemoryConsolidationPlanData, ThrowOnError>): RequestResult<GenerateMemoryConsolidationPlanResponses, GenerateMemoryConsolidationPlanErrors, ThrowOnError> => (options?.client ?? client).post<GenerateMemoryConsolidationPlanResponses, GenerateMemoryConsolidationPlanErrors, ThrowOnError>({
url: '/api/v1/user-memory/consolidation/plans',
...options,
headers: {
'Content-Type': 'application/json',
...options?.headers
}
});

export const decideMemoryConsolidationPlan = <ThrowOnError extends boolean = false>(options: Options<DecideMemoryConsolidationPlanData, ThrowOnError>): RequestResult<DecideMemoryConsolidationPlanResponses, DecideMemoryConsolidationPlanErrors, ThrowOnError> => (options.client ?? client).post<DecideMemoryConsolidationPlanResponses, DecideMemoryConsolidationPlanErrors, ThrowOnError>({
url: '/api/v1/user-memory/consolidation/plans/{planId}/decisions',
Expand Down
10 changes: 6 additions & 4 deletions apps/contracts/src/generated/types.gen.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2959,7 +2959,9 @@ export type ReflectSessionResponses = {
export type ReflectSessionResponse = ReflectSessionResponses[keyof ReflectSessionResponses];

export type GenerateMemoryConsolidationPlanData = {
body?: never;
body?: {
target?: 'user_model' | 'project_memory';
};
path?: never;
query?: never;
url: '/api/v1/user-memory/consolidation/plans';
Expand Down Expand Up @@ -3005,7 +3007,7 @@ export type GenerateMemoryConsolidationPlanError = GenerateMemoryConsolidationPl

export type GenerateMemoryConsolidationPlanResponses = {
/**
* A bounded daemon-curated user-memory consolidation plan.
* A bounded daemon-curated consolidation plan for the requested memory store.
*/
201: {
expiresAt: string;
Expand All @@ -3031,7 +3033,7 @@ export type GenerateMemoryConsolidationPlanResponses = {
}>;
plannedOperationCount: number;
plannedSourceCount: number;
target: 'user_model';
target: 'user_model' | 'project_memory';
};
};

Expand Down Expand Up @@ -3131,7 +3133,7 @@ export type DecideMemoryConsolidationPlanResponses = {
id: string;
planned: number;
skipped: number;
target: 'user_model';
target: 'user_model' | 'project_memory';
};
};

Expand Down
19 changes: 17 additions & 2 deletions apps/contracts/src/schemas/knowledge.ts
Original file line number Diff line number Diff line change
Expand Up @@ -151,6 +151,12 @@ export const reflectionReceiptSchema = z.object({
staged: z.number().int().nonnegative(),
});

/**
* The two memory stores a consolidation plan can target: facts about the
* operator (`user_model`) and the per-project store (`project_memory`).
*/
export const memoryConsolidationTargetSchema = z.enum(["user_model", "project_memory"]);

export const memoryConsolidationParticipantSchema = z.object({
description: z.string(),
key: z.string(),
Expand All @@ -172,7 +178,12 @@ export const memoryConsolidationPlanSchema = z.object({
operations: z.array(memoryConsolidationOperationSchema),
plannedOperationCount: z.number().int().nonnegative(),
plannedSourceCount: z.number().int().nonnegative(),
target: z.literal("user_model"),
target: memoryConsolidationTargetSchema,
});

/** Omitting the body, or its target, consolidates `user_model` as before. */
export const generateMemoryConsolidationPlanRequestSchema = z.object({
target: memoryConsolidationTargetSchema.default("user_model"),
});

export const decideMemoryConsolidationPlanRequestSchema = z.object({
Expand All @@ -187,7 +198,7 @@ export const memoryConsolidationReceiptSchema = z.object({
id: z.string(),
planned: z.number().int().nonnegative(),
skipped: z.number().int().nonnegative(),
target: z.literal("user_model"),
target: memoryConsolidationTargetSchema,
});

export const memoryEntrySchema = z.object({
Expand Down Expand Up @@ -226,6 +237,9 @@ export type DecideLearningProposalRequest = z.infer<typeof decideLearningProposa
export type DecideMemoryConsolidationPlanRequest = z.infer<
typeof decideMemoryConsolidationPlanRequestSchema
>;
export type GenerateMemoryConsolidationPlanRequest = z.infer<
typeof generateMemoryConsolidationPlanRequestSchema
>;
export type LearnedSkillActionRequest = z.infer<typeof learnedSkillActionRequestSchema>;
export type LearnedSkillChangesResponse = z.infer<typeof learnedSkillChangesResponseSchema>;
export type LearnedSkillDiffResponse = z.infer<typeof learnedSkillDiffResponseSchema>;
Expand All @@ -236,6 +250,7 @@ export type LearningProposalResponse = z.infer<typeof learningProposalSchema>;
export type LearningProposalsResponse = z.infer<typeof learningProposalsResponseSchema>;
export type MemoryConsolidationPlanResponse = z.infer<typeof memoryConsolidationPlanSchema>;
export type MemoryConsolidationReceiptResponse = z.infer<typeof memoryConsolidationReceiptSchema>;
export type MemoryConsolidationTarget = z.infer<typeof memoryConsolidationTargetSchema>;
export type MemoryDetailResponse = z.infer<typeof memoryDetailResponseSchema>;
export type ReflectionReceiptResponse = z.infer<typeof reflectionReceiptSchema>;
export type UndoLearningPromotionRequest = z.infer<typeof undoLearningPromotionRequestSchema>;
Expand Down
15 changes: 10 additions & 5 deletions apps/server/src/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -364,15 +364,20 @@ export function knowledgeCapabilities(runtime: MecatlRuntime): KnowledgeCapabili
} catch {
capabilities = undefined;
}
const dream = capabilities?.manualDream?.userModel;
const consolidation = (
dream: { decide: boolean; generate: boolean; unavailableReason?: string } | undefined,
) => ({
decide: dream?.decide ?? false,
generate: dream?.generate ?? false,
unavailableReason:
dream?.unavailableReason ?? "Memory consolidation is not enabled on this deployment.",
});
return {
learnedSkills: capabilities?.learnedSkills ?? false,
learningProposals: capabilities?.learningProposals ?? false,
memoryConsolidation: {
decide: dream?.decide ?? false,
generate: dream?.generate ?? false,
unavailableReason:
dream?.unavailableReason ?? "Memory consolidation is not enabled on this deployment.",
project_memory: consolidation(capabilities?.manualDream?.projectMemory),
user_model: consolidation(capabilities?.manualDream?.userModel),
},
reflection: capabilities?.reflection ?? false,
skills: capabilities?.skills ?? false,
Expand Down
140 changes: 135 additions & 5 deletions apps/server/src/mecatl/knowledge.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,53 @@ function sdkSkill(over: Record<string, unknown> = {}) {
};
}

const allConsolidation = {
project_memory: { decide: true, generate: true, unavailableReason: "" },
user_model: { decide: true, generate: true, unavailableReason: "" },
};

const allCapabilities = {
learnedSkills: true,
learningProposals: true,
memoryConsolidation: allConsolidation,
reflection: true,
skills: true,
userModel: true,
};

function sdkDreamPlan(target: string) {
return {
expiresAt: { nanos: 0, seconds: 1_700_000_000n },
id: "plan-1",
operations: [
{
exactDuplicateEligible: true,
kind: "merge",
reason: "duplicates",
replacement: { description: "Merged", value: "Uses vim" },
sources: [{ description: "Older", key: "editor_2", value: "vim" }],
survivor: { description: "Editor", key: "editor", value: "Uses vim" },
},
],
plannedOperationCount: 1,
plannedSourceCount: 2,
target,
};
}

function sdkDreamReceipt(target: string) {
return {
appliedSourceCount: 1,
conflictedSourceCount: 1,
disposition: "applied",
failedSourceCount: 0,
id: "plan-1",
plannedSourceCount: 2,
skippedSourceCount: 0,
target,
};
}

describe("Mecatl knowledge adapter", () => {
it("derives the knowledge capability set from the live runtime snapshot", async () => {
const snapshot = sampleSnapshot();
Expand All @@ -36,6 +83,11 @@ describe("Mecatl knowledge adapter", () => {
learnedSkills: true,
learningProposals: false,
manualDream: {
projectMemory: {
decide: false,
generate: false,
unavailableReason: "target store is unavailable",
},
userModel: { decide: true, generate: false, unavailableReason: "needs a model" },
},
reflection: true,
Expand All @@ -46,7 +98,14 @@ describe("Mecatl knowledge adapter", () => {
expect(live).toEqual({
learnedSkills: true,
learningProposals: false,
memoryConsolidation: { decide: true, generate: false, unavailableReason: "needs a model" },
memoryConsolidation: {
project_memory: {
decide: false,
generate: false,
unavailableReason: "target store is unavailable",
},
user_model: { decide: true, generate: false, unavailableReason: "needs a model" },
},
reflection: true,
skills: false,
userModel: true,
Expand All @@ -62,7 +121,18 @@ describe("Mecatl knowledge adapter", () => {
expect(pending).toMatchObject({
learnedSkills: false,
learningProposals: false,
memoryConsolidation: { decide: false, generate: false },
memoryConsolidation: {
project_memory: {
decide: false,
generate: false,
unavailableReason: "Memory consolidation is not enabled on this deployment.",
},
user_model: {
decide: false,
generate: false,
unavailableReason: "Memory consolidation is not enabled on this deployment.",
},
},
reflection: false,
skills: false,
userModel: false,
Expand Down Expand Up @@ -107,7 +177,7 @@ describe("Mecatl knowledge adapter", () => {
const service = createMecatlKnowledgeService({ learnedSkills } as unknown as Client, {
learnedSkills: true,
learningProposals: true,
memoryConsolidation: { decide: true, generate: true, unavailableReason: "" },
memoryConsolidation: allConsolidation,
reflection: true,
skills: true,
userModel: true,
Expand Down Expand Up @@ -204,7 +274,7 @@ describe("Mecatl knowledge adapter", () => {
{
learnedSkills: true,
learningProposals: true,
memoryConsolidation: { decide: true, generate: true, unavailableReason: "" },
memoryConsolidation: allConsolidation,
reflection: true,
skills: true,
userModel: true,
Expand All @@ -230,7 +300,7 @@ describe("Mecatl knowledge adapter", () => {
{
learnedSkills: true,
learningProposals: true,
memoryConsolidation: { decide: true, generate: true, unavailableReason: "" },
memoryConsolidation: allConsolidation,
reflection: true,
skills: true,
userModel: true,
Expand All @@ -254,4 +324,64 @@ describe("Mecatl knowledge adapter", () => {
});
expect(activate).toHaveBeenCalledTimes(1);
});

it("generates a consolidation plan for either target and keeps the daemon's target", async () => {
for (const target of ["user_model", "project_memory"] as const) {
const generate = vi.fn().mockResolvedValue({ plan: sdkDreamPlan(target) });
const service = createMecatlKnowledgeService(
{ dreamPlans: { generate } } as unknown as Client,
allCapabilities,
);
const plan = await service.generateMemoryConsolidationPlan(target);
expect(generate).toHaveBeenCalledWith({
$typeName: "mecatl.v1.GenerateDreamPlanRequest",
target,
});
expect(plan).toMatchObject({
expiresAt: "2023-11-14T22:13:20.000Z",
id: "plan-1",
operations: [{ sources: [{ key: "editor_2" }], survivor: { key: "editor" } }],
plannedOperationCount: 1,
plannedSourceCount: 2,
target,
});
}
});

it("maps decision receipts for either target and rejects an unknown target", async () => {
for (const target of ["user_model", "project_memory"] as const) {
const decide = vi.fn().mockResolvedValue({ receipt: sdkDreamReceipt(target) });
const service = createMecatlKnowledgeService(
{ dreamPlans: { decide } } as unknown as Client,
allCapabilities,
);
await expect(
service.decideMemoryConsolidationPlan("plan-1", { decision: "apply" }),
).resolves.toEqual({
applied: 1,
conflicted: 1,
disposition: "applied",
failed: 0,
id: "plan-1",
planned: 2,
skipped: 0,
target,
});
}
const service = createMecatlKnowledgeService(
{
dreamPlans: {
decide: vi.fn().mockResolvedValue({ receipt: sdkDreamReceipt("team_memory") }),
generate: vi.fn().mockResolvedValue({ plan: sdkDreamPlan("team_memory") }),
},
} as unknown as Client,
allCapabilities,
);
await expect(
service.decideMemoryConsolidationPlan("plan-1", { decision: "dismiss" }),
).rejects.toThrow("memory consolidation receipt for another target");
await expect(service.generateMemoryConsolidationPlan("user_model")).rejects.toThrow(
"memory consolidation plan for another target",
);
});
});
Loading
Loading