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

+40 -12
View File
@@ -1,7 +1,7 @@
import type { DatabaseSync } from "node:sqlite";
import { optionalEnum, optionalLimit, requireString } from "./agent-tool-args";
import { optionalEnum, optionalLimit, optionalText, requireString } from "./agent-tool-args";
import type { AgentToolContext, AgentToolDeps } from "./agent-tools";
import { KnowledgeService } from "./knowledge-service";
import { APPLICABLE_KNOWLEDGE_SQL, applicableKnowledgeArgs, KnowledgeService, type KnowledgeReader } from "./knowledge-service";
import type { JsonRecord } from "./provider-http";
import type { ToolDefinition } from "./provider-tool-turn";
import { relevantItems } from "./relevance";
@@ -9,6 +9,7 @@ import { relevantItems } from "./relevance";
const PROMPT_ENTRY_LIMIT = 5;
const PROMPT_ENTRY_CHARS = 600;
const IMPORTANCE = ["low", "normal", "high"] as const;
const SCOPES = ["organization", "project", "goal", "task", "agent"] as const;
interface KnowledgeRow {
id: string;
@@ -16,6 +17,8 @@ interface KnowledgeRow {
category: string;
content: string;
importance: string;
scope_type: string;
valid_until: string | null;
updated_at: string;
}
@@ -35,7 +38,7 @@ export const KNOWLEDGE_TOOLS: ToolDefinition[] = [
},
{
name: "record_knowledge",
description: "Record something the company should remember: a decision, a preference, a verified fact, or why something failed.",
description: "Record something the company should remember: a decision, a preference, a verified fact, or why something failed. Scope it to where it applies; pass supersedes to replace an outdated entry.",
parameters: {
type: "object",
additionalProperties: false,
@@ -44,6 +47,9 @@ export const KNOWLEDGE_TOOLS: ToolDefinition[] = [
content: { type: "string", description: "The knowledge itself, self-contained." },
category: { type: "string", description: "e.g. decision, preference, fact, lesson." },
importance: { type: "string", enum: ["low", "normal", "high"] },
scope: { type: "string", enum: [...SCOPES], description: "Where it applies: the whole company (default), this task's project, goal, or task, or only you (agent)." },
valid_until: { type: "string", description: "UTC ISO time after which it no longer applies." },
supersedes: { type: "string", description: "id of an older entry this one replaces." },
},
required: ["topic", "content", "category"],
},
@@ -53,13 +59,14 @@ export const KNOWLEDGE_TOOLS: ToolDefinition[] = [
export function searchKnowledgeTool(deps: AgentToolDeps, ctx: AgentToolContext, args: JsonRecord): string {
const query = requireString(args.query, "query");
const limit = optionalLimit(args.limit, "limit", 10, 30);
const found = relevantItems(knowledgeRows(deps.db, ctx.orgId), describeEntry, query, limit);
const found = relevantItems(knowledgeRows(deps.db, readerOf(ctx)), describeEntry, query, limit);
if (found.length === 0) return `No knowledge matches "${query}".`;
return found.map(formatEntry).join("\n\n");
}
export function recordKnowledgeTool(deps: AgentToolDeps, ctx: AgentToolContext, args: JsonRecord): string {
const importance = optionalEnum(args.importance, "importance", IMPORTANCE, "normal");
const scope = optionalEnum(args.scope, "scope", SCOPES, "organization") as (typeof SCOPES)[number];
const id = new KnowledgeService(deps.db).create({
orgId: ctx.orgId,
topic: requireString(args.topic, "topic"),
@@ -68,26 +75,47 @@ export function recordKnowledgeTool(deps: AgentToolDeps, ctx: AgentToolContext,
importance,
sourceType: "agent",
sourceId: ctx.agent.id,
scopeType: scope,
scopeId: scopeIdFor(ctx, scope),
validUntil: optionalText(args.valid_until, "valid_until"),
supersedesId: optionalText(args.supersedes, "supersedes"),
});
return `Recorded knowledge entry ${id}.`;
}
/** Prompt lines for the entries most relevant to a task. */
export function relevantKnowledgeLines(db: DatabaseSync, orgId: string, taskText: string): string[] {
const found = relevantItems(knowledgeRows(db, orgId), describeEntry, taskText, PROMPT_ENTRY_LIMIT);
export function relevantKnowledgeLines(db: DatabaseSync, reader: KnowledgeReader, taskText: string): string[] {
const found = relevantItems(knowledgeRows(db, reader), describeEntry, taskText, PROMPT_ENTRY_LIMIT);
if (found.length === 0) return [];
return [
"",
"Company knowledge that may apply (search_knowledge for more):",
...found.map((entry) => `- ${entry.topic} [${entry.category}, ${entry.importance}, id=${entry.id}]: ${clip(entry.content)}`),
...found.map((entry) => `- ${entry.topic} [${labels(entry)}]: ${clip(entry.content)}`),
];
}
function knowledgeRows(db: DatabaseSync, orgId: string): KnowledgeRow[] {
function knowledgeRows(db: DatabaseSync, reader: KnowledgeReader): KnowledgeRow[] {
return db.prepare(`
select id, topic, category, content, importance, updated_at from knowledge_entries
where org_id = ? order by updated_at desc
`).all(orgId) as unknown as KnowledgeRow[];
select id, topic, category, content, importance, scope_type, valid_until, updated_at from knowledge_entries
where ${APPLICABLE_KNOWLEDGE_SQL} order by updated_at desc
`).all(...applicableKnowledgeArgs(reader)) as unknown as KnowledgeRow[];
}
function readerOf(ctx: AgentToolContext): KnowledgeReader {
return { orgId: ctx.orgId, agentId: ctx.agent.id, projectId: ctx.taskProjectId, goalId: ctx.taskGoalId, taskId: ctx.taskId };
}
function scopeIdFor(ctx: AgentToolContext, scope: (typeof SCOPES)[number]): string | null {
if (scope === "organization") return null;
if (scope === "agent") return ctx.agent.id;
const id = scope === "project" ? ctx.taskProjectId : scope === "goal" ? ctx.taskGoalId : ctx.taskId;
if (!id) throw new Error(`This run has no ${scope} to scope knowledge to.`);
return id;
}
function labels(entry: KnowledgeRow): string {
const validity = entry.valid_until ? `, valid until ${entry.valid_until}` : "";
return `${entry.category}, ${entry.importance}, ${entry.scope_type}${validity}, id=${entry.id}`;
}
function describeEntry(entry: KnowledgeRow): string {
@@ -95,7 +123,7 @@ function describeEntry(entry: KnowledgeRow): string {
}
function formatEntry(entry: KnowledgeRow): string {
return `${entry.topic} [${entry.category}, ${entry.importance}, id=${entry.id}, updated ${entry.updated_at}]\n${entry.content}`;
return `${entry.topic} [${labels(entry)}, updated ${entry.updated_at}]\n${entry.content}`;
}
function clip(text: string): string {