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
10 changes: 10 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,15 @@
# Changelog

## 15.0.0 — 2026-09-07

KB improvement can declare a pages directory and include canonical claim ledgers and research events in candidate state.
Snapshots, hashes, resume, selected changes, promotion, and restoration use the same declared scope.
`normalizeKnowledgeStateScope` lets consumers carry the canonical scope without duplicating its defaults or validation.
Existing calls retain the default page, raw-source, and source-registry scope.
Configured tool receipt capture now persists exact visibility artifacts in the run store before recording receipts.
The Eval peer range also admits 0.176 for complete optimization history and final measurement receipts.
The major version follows the package gate for changed exported contract shapes.

## 14.0.3 — 2026-09-07

The Eval peer range admits both 0.174 and 0.175, restoring compatibility with Eval 0.174 consumers.
Expand Down
19 changes: 19 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -318,6 +318,25 @@ A failed final evaluation ends the run instead of selecting another candidate ag
The default evaluator reports only measured dimensions and averages those dimensions with equal weight.
It omits `answer_quality` without answer evaluation, `promotion_decision` without a promotion decision, and `blocking_readiness` without blocking readiness requirements.
Default evaluator version `2` records this weighting.
Declare additional authoritative local state through `stateScope`:

```ts
const stateScope = { pagesDirectory: 'kb/pages', researchState: true }
const result = await improveKnowledgeBase({ ...options, stateScope })
const identity = await hashKnowledgeBase(root, stateScope)
```

The default scope remains `knowledge/`, `raw/`, and `.agent-knowledge/sources.json`.
`pagesDirectory` replaces the default page directory for copying, indexing, hashing, and promotion.
`researchState: true` also includes canonical `.agent-knowledge/claim-ledgers/` records and `.agent-knowledge/events.json`.
The persisted run binds this scope, and resume rejects a different scope.
Candidate materialization, selected changes, promotion, and restoration preserve the same declared state.
Use `normalizeKnowledgeStateScope` when carrying this scope into another contract.
Indexes, locks, transaction journals, retrieval artifacts, credentials outside declared content directories, and external memory providers remain outside this identity.
Use the canonical `FileSystemKbStore({ root: candidateRoot })` for research records in update callbacks.
Callbacks must bind external state separately and must not close over a shared mutable store when evaluating isolated candidates.
A scoped KB hash does not identify an entire learner with external state.

A candidate can pass structural checks without any task outcome evaluation; the metric notes state this limit.
`candidate-ready` means the configured checks passed and the candidate remains detached from the live knowledge base.

Expand Down
16 changes: 9 additions & 7 deletions api-surface.json
Original file line number Diff line number Diff line change
Expand Up @@ -201,7 +201,7 @@
"KnowledgeControlLoopAction": "value 278d5c67af20",
"KnowledgeControlLoopActionResult": "value cf2e78a26708",
"KnowledgeControlLoopAdapter": "value 4e5907588233",
"KnowledgeControlLoopAdapterOptions": "value cbc0fec0e6d8",
"KnowledgeControlLoopAdapterOptions": "value eec2b80a8c88",
"KnowledgeControlLoopState": "value 8343ad013bdc",
"KnowledgeDiscoveryDispatcher": "value a4ed7f82b891",
"KnowledgeDiscoveryWorker": "value 704c91abebe2",
Expand Down Expand Up @@ -235,12 +235,12 @@
"KnowledgeImprovementMetricProvenance": "type 8c9bdc8396db",
"KnowledgeImprovementMutationReceipt": "type b7756b2e1432",
"KnowledgeImprovementMutationResult": "type 9b65834cf2e3",
"KnowledgeImprovementOptions": "type e10d24cb9e5c",
"KnowledgeImprovementOptions": "type f295268a3492",
"KnowledgeImprovementRagOptimizationOptions": "type 57b600037905",
"KnowledgeImprovementRagOptimizationRunInput": "type 0bba567069ac",
"KnowledgeImprovementResult": "type a457d844fd3d",
"KnowledgeImprovementRetrievalOptions": "type ec2bec026699",
"KnowledgeImprovementRunState": "type 90928bbe8ecc",
"KnowledgeImprovementRunState": "type 1ae3b226bbdd",
"KnowledgeImprovementRunStateSchema": "value 373728f5643d",
"KnowledgeImprovementStatus": "type 0b4aedbceeb7",
"KnowledgeImprovementTarget": "type 2a1bc73ffa39",
Expand Down Expand Up @@ -313,6 +313,7 @@
"KnowledgeSearchHit": "value d13a101a4b4f",
"KnowledgeSearchResult": "value c443b7efb49e",
"KnowledgeSource": "value 6ebb3f0a0ef2",
"KnowledgeStateScope": "type 61b8ac32e052",
"KnowledgeUnit": "value bbd850260f7f",
"KnowledgeUseReceipt": "value 95b469ec3c07",
"KnowledgeUseRelation": "value b47137088743",
Expand Down Expand Up @@ -419,8 +420,8 @@
"ResearchSourceVersion": "value f20d55e99f71",
"ResearchSourceVersionSchema": "value 373728f5643d",
"ResearchWorker": "value 5b69df6c9975",
"ResolvedKnowledgeImprovementCandidate": "type d0a1c8976ee6",
"ResolvedKnowledgeImprovementComparison": "type 45197417f12c",
"ResolvedKnowledgeImprovementCandidate": "type 60be04dfb75d",
"ResolvedKnowledgeImprovementComparison": "type 2d4520e11c17",
"ResolvedKnowledgeImprovementComparisonSnapshot": "type 09ca4bdc3f81",
"RestoreKnowledgeCandidateBaselineOptions": "type a910d66fbc6b",
"RetrievalConfig": "value 0f5f3a0fc625",
Expand Down Expand Up @@ -459,7 +460,7 @@
"RunDiscoveryLoopOptions": "value bfcdb3617601",
"RunKnowledgeBenchmarkSuiteOptions": "type 8f7c9a385609",
"RunKnowledgeBenchmarkSuiteResult": "type 46bed11cb991",
"RunKnowledgeResearchLoopOptions": "value b47742289fe2",
"RunKnowledgeResearchLoopOptions": "value ac668b866256",
"RunLineageAuthority": "value aa33159b2c96",
"RunLineageRecord": "value 9ba2102ed82c",
"RunMemoryAdapterBenchmarkOptions": "type 9e6710497bb1",
Expand Down Expand Up @@ -623,7 +624,7 @@
"gradeFor": "value 79db4a5137a3",
"graphitiMemoryAdapterIdentity": "value 203f93807ab2",
"groundClaimInText": "value f74ed9ca26c6",
"hashKnowledgeBase": "value 701cd37f4e5b",
"hashKnowledgeBase": "value bdb2d9eea5f9",
"htmlToText": "value c58c3f5d8c75",
"improveKnowledgeBase": "value a4ebce095bdf",
"improveSelectedKnowledgeCandidate": "value 6d48e36af283",
Expand Down Expand Up @@ -679,6 +680,7 @@
"neighbors": "value 29f67f6cea95",
"normalizeClaimText": "value 4c9eca896a37",
"normalizeExternalRagScores": "value 1a2d603c9282",
"normalizeKnowledgeStateScope": "value beef86fc6f66",
"normalizeLinkTarget": "value 0258f337b3b3",
"normalizePageText": "value 9c20decc8888",
"normalizePagesDirectory": "value 7af1695dc6cf",
Expand Down
11 changes: 11 additions & 0 deletions docs/architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,17 @@ Every write in this layer goes through `durable-fs` (`writeFileDurable`, `writeJ
`O_NOFOLLOW` descriptors anchored through `/proc/self/fd` prevent a directory swapped for a symlink during a write from redirecting it outside the root.
These are exported from the package entrypoint; consumers that keep their own journals should use them rather than reimplement them.

## Candidate state scope

KB improvement snapshots include pages, raw evidence, and the source registry by default.
A declared `stateScope.pagesDirectory` selects the same pages for writers, readers, indexing, and promotion.
Opting into `stateScope.researchState` also binds canonical claim ledgers and research events.
The hash includes nondefault scope declarations, so two different scopes cannot silently share an identity when their extra directories are empty.
Resume uses the persisted scope and refuses a changed declaration.
Promotion and crash recovery apply its narrow path allowlist through the existing file transaction journal.
Derived indexes, lock state, retrieval artifacts, and external providers are excluded.
Applications must bind external state through their existing memory branch or evaluation contracts.

## Runtime Loop

1. Normalize sources into immutable source records.
Expand Down
10 changes: 10 additions & 0 deletions docs/knowledge-use-receipts.md
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,16 @@ const artifact = knowledgeVisibilityArtifactRef({ uri: 'artifact://run/visibilit

The artifact reference is optional at this contract layer: a caller that retains the snapshot in another durable record may omit it. A production adapter should require a durable locator.

When `createKnowledgeTools` has a `recordRetrieval` sink, it persists canonical visibility bytes before calling that sink.
It stores artifacts under the run's `.agent-knowledge/retrieval-visibility/` directory and attaches a `file:` artifact locator to each receipt.
Searches over the same view reuse that artifact, including concurrent searches.
Changed views receive different artifacts.
A persistence failure or conflicting stored bytes prevents receipt delivery.
The host must retain these artifacts with its receipts and make the locator accessible to later verification.
Searches without a receipt sink still return an in-memory receipt without creating durable evidence.
These artifacts record visibility; they do not promote observations into sourced claims or prove downstream utility.


## Retrieval receipt

`createKnowledgeRetrievalReceipt()` binds:
Expand Down
4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@tangle-network/agent-knowledge",
"version": "14.0.3",
"version": "15.0.0",
"description": "Build, search, evaluate, and improve source-backed knowledge bases.",
"homepage": "https://github.com/tangle-network/agent-knowledge#readme",
"repository": {
Expand Down Expand Up @@ -84,7 +84,7 @@
"zod": "4.5.4"
},
"peerDependencies": {
"@tangle-network/agent-eval": ">=0.174.0 <0.176.0",
"@tangle-network/agent-eval": ">=0.174.0 <0.177.0",
"@tangle-network/agent-interface": "^2.0.0"
},
"devDependencies": {
Expand Down
2 changes: 1 addition & 1 deletion scripts/verify-official-optimizers.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ const agentEvalVersion = sourcePackage.devDependencies?.['@tangle-network/agent-
if (!/^\d+\.\d+\.\d+$/.test(agentEvalVersion)) {
throw new Error('@tangle-network/agent-eval must have one exact development pin')
}
const expectedEvalPeerRange = '>=0.174.0 <0.176.0'
const expectedEvalPeerRange = '>=0.174.0 <0.177.0'
if (sourcePackage.peerDependencies?.['@tangle-network/agent-eval'] !== expectedEvalPeerRange) {
throw new Error(
`@tangle-network/agent-eval peer range must be ${expectedEvalPeerRange} to match the development pin`,
Expand Down
5 changes: 3 additions & 2 deletions scripts/verify-package.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ const publicImports = [
]
const requiredRootExports = [
'createFileSystemSearchProvider',
'normalizeKnowledgeStateScope',
'optimizeKnowledgeBasePolicy',
'runRagOptimization',
'runRetrievalImprovementLoop',
Expand Down Expand Up @@ -68,10 +69,10 @@ const repoRoot = resolve(dirname(fileURLToPath(import.meta.url)), '..')
const sourcePackage = JSON.parse(readFileSync(join(repoRoot, 'package.json'), 'utf8'))
const agentEvalVersion =
process.env.AGENT_KNOWLEDGE_EVAL_VERSION ?? exactDevelopmentPin(sourcePackage, agentEvalPackage)
if (!['0.174.0', '0.175.0'].includes(agentEvalVersion)) {
if (!['0.174.0', '0.175.0', '0.176.0'].includes(agentEvalVersion)) {
throw new Error(`unsupported Eval compatibility test version: ${agentEvalVersion}`)
}
const agentEvalPeerRange = '>=0.174.0 <0.176.0'
const agentEvalPeerRange = '>=0.174.0 <0.177.0'
const agentInterfaceVersion = exactDevelopmentPin(sourcePackage, agentInterfacePackage)
const agentInterfacePeerRange = expectedPeerRange(agentInterfaceVersion)
const zodVersion = exactVersion(sourcePackage.dependencies?.zod, 'zod runtime dependency')
Expand Down
39 changes: 33 additions & 6 deletions src/file-transaction.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ const transactionSchema = z
purpose: z.string().min(1),
recoveryOwner: z.string().min(1).max(256).optional(),
pagesDirectory: pagesDirectorySchema.optional(),
researchState: z.boolean().optional(),
createdAt: z.string().min(1),
entries: z.array(transactionEntrySchema).min(1),
})
Expand Down Expand Up @@ -85,9 +86,10 @@ export interface KnowledgeFileTransactionPlanEntry {
export function knowledgeFileTransactionPlanHash(
entries: readonly KnowledgeFileTransactionPlanEntry[],
pagesDirectory: string,
researchState = false,
): string {
const normalized = entries
.map((entry) => normalizePlanEntry(entry, pagesDirectory))
.map((entry) => normalizePlanEntry(entry, pagesDirectory, researchState))
.sort((left, right) => left.path.localeCompare(right.path))
if (new Set(normalized.map((entry) => entry.path)).size !== normalized.length) {
throw new Error('knowledge transaction plan repeats a path')
Expand All @@ -109,6 +111,8 @@ export async function prepareKnowledgeFileTransaction(input: {
mutations: readonly KnowledgeFileMutation[]
/** Pages directory the mutations may write under; defaults to `knowledge`. */
pagesDirectory?: string
/** Explicitly permit authoritative claim-ledger and research-event records. */
researchState?: boolean
includeUnchanged?: boolean
now?: () => Date
}): Promise<KnowledgeFileTransaction | null> {
Expand All @@ -129,7 +133,11 @@ export async function prepareKnowledgeFileTransaction(input: {
const paths = new Set<string>()
const prepared = await Promise.all(
input.mutations.map(async (mutation, index) => {
const path = assertKnowledgeMutationPath(mutation.path, boundPagesDirectory)
const path = assertKnowledgeMutationPath(
mutation.path,
boundPagesDirectory,
input.researchState,
)
if (paths.has(path)) throw new Error(`knowledge file transaction repeats path: ${path}`)
if (mutation.content === null && mutation.mode !== undefined) {
throw new Error(`deleted knowledge file cannot declare a mode: ${path}`)
Expand Down Expand Up @@ -192,6 +200,7 @@ export async function prepareKnowledgeFileTransaction(input: {
purpose: input.purpose,
...(input.recoveryOwner ? { recoveryOwner: input.recoveryOwner } : {}),
...(pagesDirectory === undefined ? {} : { pagesDirectory }),
...(input.researchState === undefined ? {} : { researchState: input.researchState }),
createdAt: (input.now ?? (() => new Date()))().toISOString(),
entries: changed.map((item) => item.entry),
})
Expand Down Expand Up @@ -222,6 +231,8 @@ export async function commitKnowledgeFileMutations(input: {
mutations: readonly KnowledgeFileMutation[]
/** Pages directory the mutations may write under; defaults to `knowledge`. */
pagesDirectory?: string
/** Explicitly permit authoritative claim-ledger and research-event records. */
researchState?: boolean
assertOwned?: () => void
now?: () => Date
}): Promise<boolean> {
Expand All @@ -238,6 +249,7 @@ export async function commitKnowledgeFileMutations(input: {
transactionRoot: input.transactionRoot,
purpose: input.purpose,
mutations: input.mutations,
...(input.researchState === undefined ? {} : { researchState: input.researchState }),
...(input.pagesDirectory === undefined ? {} : { pagesDirectory: input.pagesDirectory }),
now: input.now,
})
Expand Down Expand Up @@ -630,7 +642,11 @@ function assertTransactionEntries(transaction: KnowledgeFileTransaction): void {
const paths = new Set<string>()
const boundPagesDirectory = normalizePagesDirectory(transaction.pagesDirectory)
for (const entry of transaction.entries) {
const normalized = assertKnowledgeMutationPath(entry.path, boundPagesDirectory)
const normalized = assertKnowledgeMutationPath(
entry.path,
boundPagesDirectory,
transaction.researchState,
)
if (entry.path !== normalized || indexes.has(entry.index) || paths.has(entry.path)) {
throw new Error('knowledge file transaction has duplicate or unsafe entries')
}
Expand All @@ -641,8 +657,12 @@ function assertTransactionEntries(transaction: KnowledgeFileTransaction): void {
}
}

function normalizePlanEntry(entry: KnowledgeFileTransactionPlanEntry, pagesDirectory: string) {
const path = assertKnowledgeMutationPath(entry.path, pagesDirectory)
function normalizePlanEntry(
entry: KnowledgeFileTransactionPlanEntry,
pagesDirectory: string,
researchState: boolean,
) {
const path = assertKnowledgeMutationPath(entry.path, pagesDirectory, researchState)
assertHashModePair(path, 'before', entry.beforeHash, entry.beforeMode)
assertHashModePair(path, 'after', entry.afterHash, entry.afterMode)
return {
Expand Down Expand Up @@ -683,10 +703,17 @@ export function knowledgeMutationPathPrefixes(pagesDirectory: string): readonly
return [`${normalizePagesDirectory(pagesDirectory)}/`, 'raw/']
}

export function assertKnowledgeMutationPath(path: string, pagesDirectory: string): string {
export function assertKnowledgeMutationPath(
path: string,
pagesDirectory: string,
researchState = false,
): string {
const normalized = normalizeTransactionPath(path)
if (
normalized === SOURCE_REGISTRY_PATH ||
(researchState &&
(normalized === '.agent-knowledge/events.json' ||
/^\.agent-knowledge\/claim-ledgers\/[^/]+\.json$/.test(normalized))) ||
knowledgeMutationPathPrefixes(pagesDirectory).some((prefix) => normalized.startsWith(prefix))
) {
return normalized
Expand Down
4 changes: 4 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,10 @@ export * from './investment-thesis-task'
export * from './kb-improvement'
export * from './kb-store'
export * from './knowledge-brief'
export {
type KnowledgeStateScope,
normalizeKnowledgeStateScope,
} from './knowledge-state-scope'
export * from './knowledge-tools'
export * from './knowledge-use-receipts'
export * from './lexical-index'
Expand Down
7 changes: 7 additions & 0 deletions src/kb-improvement/contracts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import type {
EvalKnowledgeBundleBuildResult,
KnowledgeReadinessSpec,
} from '../eval-readiness'
import { type KnowledgeStateScope, knowledgeStateScopeSchema } from '../knowledge-state-scope'
import { DEFAULT_PAGES_DIRECTORY } from '../pages-directory'
import type {
KnowledgeBaseQualityOptions,
Expand Down Expand Up @@ -102,6 +103,7 @@ export interface KnowledgeImprovementCandidateRecord {
}

export interface KnowledgeImprovementRunState {
stateScope?: KnowledgeStateScope
runId: string
root: string
goal: string
Expand Down Expand Up @@ -280,6 +282,7 @@ const candidateRecordSchema = z

export const KnowledgeImprovementRunStateSchema = z
.object({
stateScope: knowledgeStateScopeSchema.optional(),
runId: runIdSchema,
root: z.string().min(1),
goal: z.string().min(1),
Expand Down Expand Up @@ -455,13 +458,15 @@ export interface ResolvedKnowledgeImprovementComparisonSnapshot {
}

export interface ResolvedKnowledgeImprovementComparison {
stateScope?: KnowledgeStateScope
reference: KnowledgeImprovementCandidateRef
evaluation: KnowledgeImprovementMetric
baseline: ResolvedKnowledgeImprovementComparisonSnapshot
candidate: ResolvedKnowledgeImprovementComparisonSnapshot
}

export interface ResolvedKnowledgeImprovementCandidate {
stateScope?: KnowledgeStateScope
root: string
candidate: KnowledgeImprovementCandidateRef
evaluation: KnowledgeImprovementMetric
Expand Down Expand Up @@ -506,6 +511,8 @@ export type KnowledgeImprovementUpdate = (
) => Promise<RagKnowledgeUpdateResult> | RagKnowledgeUpdateResult

export interface KnowledgeImprovementOptions {
/** Declared authoritative local state, frozen and restored with each candidate. */
stateScope?: KnowledgeStateScope
root: string
goal: string
/**
Expand Down
Loading