Q26: knowledge has scope, expiry, and supersession

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
ZacharyZhang-NYandClaude Opus 5.5 committed 2026-10-08 05:47:03 -04:00
1 parent 8c70111c93
commit 18128b5ad4
14 files changed
+252 -56

No files matched your search

+3
View File
@@ -56,6 +56,9 @@ describe("run memory", () => {
topic: expect.stringMatching(/^Outcome: /), category: "run_outcome",
content: "Chose Postgres for bookings; MySQL lacked the JSON features we need.", source_type: "task_run",
}]);
const project = (h.connection.db.prepare("select t.project_id from tasks t join task_runs r on r.task_id = t.id where r.id = ?").get(kickoff.id) as { project_id: string | null }).project_id;
expect(project).not.toBeNull();
expect(h.connection.db.prepare("select scope_type, scope_id from knowledge_entries").get()).toEqual({ scope_type: "project", scope_id: project });
h.connection.close();
});
+2 -2
View File
@@ -65,8 +65,8 @@ describe("task prompt scope", () => {
await execution.executeRun(kickoff!.org_id, kickoff!.id);
const task = connection.db.prepare("select id from tasks where org_id = ? and title = 'Build the booking API'").get(orgId) as { id: string };
new KnowledgeService(connection.db).create({ orgId, topic: "Booking storage", category: "decision", content: "Bookings live in Postgres.", importance: "high", sourceType: "employee", sourceId: null });
new KnowledgeService(connection.db).create({ orgId, topic: "Offsite venue", category: "fact", content: "Lisbon in May.", importance: "low", sourceType: "employee", sourceId: null });
new KnowledgeService(connection.db).create({ orgId, topic: "Booking storage", category: "decision", content: "Bookings live in Postgres.", importance: "high", sourceType: "employee", sourceId: null, scopeType: "organization", scopeId: null, validUntil: null, supersedesId: null });
new KnowledgeService(connection.db).create({ orgId, topic: "Offsite venue", category: "fact", content: "Lisbon in May.", importance: "low", sourceType: "employee", sourceId: null, scopeType: "organization", scopeId: null, validUntil: null, supersedesId: null });
await execution.runTask({ orgId, taskId: task.id });
const danaId = (connection.db.prepare("select id from agents where org_id = ? and name = 'Dana Lee'").get(orgId) as { id: string }).id;
+92 -24
View File
@@ -7,56 +7,124 @@ import { OrganizationService } from "../../src/main/services/organization-servic
import { ChatService } from "../../src/main/services/chat-service";
import { dispatchAgentTool, type AgentToolContext } from "../../src/main/services/agent-tools";
import { relevantKnowledgeLines } from "../../src/main/services/agent-knowledge-tools";
import { RISK_LEVEL, TASK_PRIORITY, TASK_STATUS } from "../../src/shared/domain";
function harness() {
const connection = new DatabaseConnection(join(tmpdir(), `ac-knowledge-${randomUUID()}.db`));
const organization = new OrganizationService(connection.db);
let orgId = "";
let agentId = "";
let otherAgentId = "";
let projectA = "";
let projectB = "";
let taskA = "";
connection.transaction(() => {
orgId = organization.createOrganization({ organizationName: "K", employeeName: "Y", mission: "m", goal: "g" });
agentId = organization.createAgent({ orgId, name: "Ana", role: "Engineer", department: "Engineering", modelProviderId: null, modelId: null, budgetCents: 1000, managerAgentId: null });
otherAgentId = organization.createAgent({ orgId, name: "Ben", role: "Engineer", department: "Engineering", modelProviderId: null, modelId: null, budgetCents: 1000, managerAgentId: null });
projectA = organization.createProject({ orgId, name: "Bookings", description: "d", goalId: null, leadAgentId: null });
projectB = organization.createProject({ orgId, name: "Billing", description: "d", goalId: null, leadAgentId: null });
taskA = organization.createTask({
orgId, title: "Build bookings", description: "d", status: TASK_STATUS.BACKLOG, assigneeAgentId: agentId,
risk: RISK_LEVEL.LOW, projectId: projectA, goalId: null, workspaceId: null, priority: TASK_PRIORITY.NORMAL, requiresFilesystem: false,
});
});
const ctx: AgentToolContext = {
orgId, agent: { id: agentId, modelProviderId: null, modelId: null },
taskProjectId: null, taskGoalId: null, taskId: null, sourceThreadId: null, workspace: null,
};
const run = (name: string, args: Record<string, unknown>) =>
dispatchAgentTool({ db: connection.db, organization, chat: new ChatService(connection.db) }, ctx, { id: `c_${randomUUID()}`, name, arguments: args });
return { connection, orgId, agentId, run };
const ctx = (agent: string, projectId: string | null, taskId: string | null): AgentToolContext => ({
orgId, agent: { id: agent, modelProviderId: null, modelId: null },
taskProjectId: projectId, taskGoalId: null, taskId, sourceThreadId: null, workspace: null,
});
const run = (context: AgentToolContext, name: string, args: Record<string, unknown>) =>
dispatchAgentTool({ db: connection.db, organization, chat: new ChatService(connection.db) }, context, { id: `c_${randomUUID()}`, name, arguments: args });
const reader = (agent: string, projectId: string | null, taskId: string | null) => ({ orgId, agentId: agent, projectId, goalId: null, taskId });
return { connection, orgId, agentId, otherAgentId, projectA, projectB, taskA, ctx, run, reader };
}
describe("knowledge in the execution chain", () => {
test("agents record knowledge and find it again by search", () => {
const h = harness();
expect(h.run("record_knowledge", { topic: "Booking storage", content: "The booking API stores orders in Postgres, not MySQL.", category: "decision", importance: "high" }))
const ana = h.ctx(h.agentId, null, null);
expect(h.run(ana, "record_knowledge", { topic: "Booking storage", content: "The booking API stores orders in Postgres, not MySQL.", category: "decision", importance: "high" }))
.toMatch(/^Recorded knowledge entry kne_/);
h.run("record_knowledge", { topic: "Brand colors", content: "Use teal and sand.", category: "preference" });
const row = h.connection.db.prepare("select source_type, source_id, importance from knowledge_entries where topic = 'Booking storage'").get();
expect(row).toEqual({ source_type: "agent", source_id: h.agentId, importance: "high" });
h.run(ana, "record_knowledge", { topic: "Brand colors", content: "Use teal and sand.", category: "preference" });
const row = h.connection.db.prepare("select source_type, source_id, importance, scope_type, scope_id from knowledge_entries where topic = 'Booking storage'").get();
expect(row).toEqual({ source_type: "agent", source_id: h.agentId, importance: "high", scope_type: "organization", scope_id: null });
const found = h.run("search_knowledge", { query: "which database for bookings postgres" });
expect(found).toContain("Booking storage [decision, high");
const found = h.run(ana, "search_knowledge", { query: "which database for bookings postgres" });
expect(found).toContain("Booking storage [decision, high, organization, id=");
expect(found).not.toContain("Brand colors");
expect(h.run("search_knowledge", { query: "kubernetes" })).toBe('No knowledge matches "kubernetes".');
expect(h.run("record_knowledge", { topic: "x", content: "y", category: "z", importance: "urgent" })).toBe('ERROR: "importance" must be one of: low, normal, high.');
expect(h.run("record_knowledge", { topic: "x", content: "y", category: "z", importance: "" })).toBe('ERROR: "importance" must be one of: low, normal, high.');
expect(h.run("search_knowledge", { query: "postgres", limit: "nonsense" })).toBe('ERROR: "limit" must be a whole number from 1 to 30.');
expect(h.run("search_knowledge", { query: "postgres", limit: 0 })).toBe('ERROR: "limit" must be a whole number from 1 to 30.');
expect(h.run("read_messages", { channel: "general", limit: 99 })).toBe('ERROR: "limit" must be a whole number from 1 to 30.');
expect(h.run(ana, "search_knowledge", { query: "kubernetes" })).toBe('No knowledge matches "kubernetes".');
expect(h.run(ana, "record_knowledge", { topic: "x", content: "y", category: "z", importance: "urgent" })).toBe('ERROR: "importance" must be one of: low, normal, high.');
expect(h.run(ana, "record_knowledge", { topic: "x", content: "y", category: "z", importance: "" })).toBe('ERROR: "importance" must be one of: low, normal, high.');
expect(h.run(ana, "search_knowledge", { query: "postgres", limit: "nonsense" })).toBe('ERROR: "limit" must be a whole number from 1 to 30.');
expect(h.run(ana, "search_knowledge", { query: "postgres", limit: 0 })).toBe('ERROR: "limit" must be a whole number from 1 to 30.');
expect(h.run(ana, "read_messages", { channel: "general", limit: 99 })).toBe('ERROR: "limit" must be a whole number from 1 to 30.');
expect(h.connection.db.prepare("select count(*) as n from knowledge_entries where topic = 'x'").get()).toEqual({ n: 0 });
h.connection.close();
});
test("only knowledge relevant to the task enters its prompt", () => {
const h = harness();
h.run("record_knowledge", { topic: "Booking storage", content: "The booking API stores orders in Postgres.", category: "decision" });
h.run("record_knowledge", { topic: "Brand colors", content: "Use teal and sand.", category: "preference" });
const lines = relevantKnowledgeLines(h.connection.db, h.orgId, "Build the booking API").join("\n");
const ana = h.ctx(h.agentId, null, null);
h.run(ana, "record_knowledge", { topic: "Booking storage", content: "The booking API stores orders in Postgres.", category: "decision" });
h.run(ana, "record_knowledge", { topic: "Brand colors", content: "Use teal and sand.", category: "preference" });
const lines = relevantKnowledgeLines(h.connection.db, h.reader(h.agentId, null, null), "Build the booking API").join("\n");
expect(lines).toContain("Company knowledge that may apply (search_knowledge for more):");
expect(lines).toContain("- Booking storage [decision, normal");
expect(lines).toContain("- Booking storage [decision, normal, organization");
expect(lines).not.toContain("Brand colors");
expect(relevantKnowledgeLines(h.connection.db, h.orgId, "Plan the offsite")).toEqual([]);
expect(relevantKnowledgeLines(h.connection.db, h.reader(h.agentId, null, null), "Plan the offsite")).toEqual([]);
h.connection.close();
});
});
describe("knowledge scope and validity", () => {
test("project, task, and agent scopes only reach readers inside them", () => {
const h = harness();
const inTask = h.ctx(h.agentId, h.projectA, h.taskA);
h.run(inTask, "record_knowledge", { topic: "Retry rule", content: "Booking payment retries twice.", category: "decision", scope: "project" });
h.run(inTask, "record_knowledge", { topic: "Draft state", content: "Booking draft lives in drafts table.", category: "fact", scope: "task" });
h.run(inTask, "record_knowledge", { topic: "My habit", content: "I prefer small booking commits.", category: "preference", scope: "agent" });
const scopes = h.connection.db.prepare("select topic, scope_type, scope_id from knowledge_entries order by topic").all();
expect(scopes).toEqual([
{ topic: "Draft state", scope_type: "task", scope_id: h.taskA },
{ topic: "My habit", scope_type: "agent", scope_id: h.agentId },
{ topic: "Retry rule", scope_type: "project", scope_id: h.projectA },
]);
const seen = (reader: ReturnType<typeof h.reader>) => relevantKnowledgeLines(h.connection.db, reader, "booking").join("\n");
const insideAll = seen(h.reader(h.agentId, h.projectA, h.taskA));
expect(["Retry rule", "Draft state", "My habit"].every((topic) => insideAll.includes(topic))).toBe(true);
const otherProject = seen(h.reader(h.otherAgentId, h.projectB, null));
expect(otherProject).toBe("");
const sameProjectOtherAgent = seen(h.reader(h.otherAgentId, h.projectA, null));
expect(sameProjectOtherAgent).toContain("Retry rule");
expect(sameProjectOtherAgent).not.toContain("Draft state");
expect(sameProjectOtherAgent).not.toContain("My habit");
expect(h.run(h.ctx(h.agentId, null, null), "record_knowledge", { topic: "x", content: "y", category: "z", scope: "project" }))
.toBe("ERROR: This run has no project to scope knowledge to.");
h.connection.close();
});
test("expired and superseded entries stop applying; replacements are tracked", () => {
const h = harness();
const ana = h.ctx(h.agentId, null, null);
h.run(ana, "record_knowledge", { topic: "Promo code", content: "Booking promo SPRING applies.", category: "fact", valid_until: "2020-01-01T00:00:00Z" });
const oldId = /kne_\w+/.exec(h.run(ana, "record_knowledge", { topic: "Booking price", content: "Booking costs 10 dollars.", category: "fact" }))![0];
const newId = /kne_\w+/.exec(h.run(ana, "record_knowledge", { topic: "Booking price", content: "Booking costs 12 dollars.", category: "fact", supersedes: oldId }))![0];
expect(h.connection.db.prepare("select superseded_by from knowledge_entries where id = ?").get(oldId)).toEqual({ superseded_by: newId });
const found = h.run(ana, "search_knowledge", { query: "booking" });
expect(found).toContain("12 dollars");
expect(found).not.toContain("10 dollars");
expect(found).not.toContain("SPRING");
expect(h.run(ana, "record_knowledge", { topic: "p", content: "q", category: "fact", supersedes: oldId }))
.toBe(`ERROR: Knowledge entry ${oldId} was already superseded by ${newId}.`);
expect(h.run(ana, "record_knowledge", { topic: "p", content: "q", category: "fact", valid_until: "next week" }))
.toBe('ERROR: "valid_until" must be a UTC ISO timestamp like 2026-12-31T00:00:00Z, got "next week".');
expect(h.run(ana, "record_knowledge", { topic: "p", content: "q", category: "fact", valid_until: "" })).toBe('ERROR: "valid_until" is required.');
expect(h.run(ana, "record_knowledge", { topic: "p", content: "q", category: "fact", supersedes: 123 })).toBe('ERROR: "supersedes" is required.');
expect(h.connection.db.prepare("select count(*) as n from knowledge_entries where topic = 'p'").get()).toEqual({ n: 0 });
h.connection.db.prepare("delete from knowledge_entries where id = ?").run(newId);
expect(h.connection.db.prepare("select superseded_by from knowledge_entries where id = ?").get(oldId)).toEqual({ superseded_by: newId });
expect(h.run(ana, "search_knowledge", { query: "booking" })).not.toContain("10 dollars");
h.connection.close();
});
});
+1 -1
View File
@@ -24,7 +24,7 @@ describe("domain services", () => {
const connection = createConnection();
const rows = connection.db.prepare("select version from schema_migrations order by version asc").all() as Array<{ version: number }>;
expect(rows.map((row) => row.version)).toEqual([1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62]);
expect(rows.map((row) => row.version)).toEqual([1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63]);
connection.close();
});