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

## 8.1.0 — 2026-08-20

### Added

- Add `buildKnowledgeRelationGraph({ nodes, relations })`, which builds a labeled multi-edge graph from `KnowledgeRelation[]` and caller-declared nodes with a `kind`, `label`, and `metadata`. The graph keeps one edge per `(sourceId, targetId, predicate)`; a repeated triple is accepted only when it is byte-identical, and an endpoint outside the declared nodes is refused.
- Add `neighbors`, `walk`, and `isReachable` over that graph, filtered by predicate and direction (`out`, `in`, `both`), with a cycle-safe breadth-first walk.
- Add `KnowledgeRelationSchema`, `KnowledgeRelationNodeSchema`, and `KnowledgeRelationGraphSchema`, with an explicit `metadata` field so a persisted graph round-trips. `KnowledgeBaseCandidateSchema` uses the same relation schema.
- Add `knowledgePageRelations(pages)`, the labeled page relations (`wikilink`, `citation`, `shared-source`, `contradicts`) that `buildKnowledgeGraph` now collapses into its weighted edges; the collapsed graph bytes are unchanged.

## 8.0.10 — 2026-08-19

### Changed
Expand Down
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,9 @@ The provider uses the package's local text search.
Pass `refresh: 'always'` to rebuild its index before every query, or call `invalidate()` after changing files.
Use `asRetrievalEvalRetriever()` to send the same search path into retrieval tests.

`knowledgePageRelations(pages)` lists the labeled relations between pages (`wikilink`, `citation`, `shared-source`, `contradicts`), and `buildKnowledgeGraph` collapses them into the weighted page graph stored in the index.
For caller-defined provenance (runs, claims, models, any predicate), `buildKnowledgeRelationGraph({ nodes, relations })` keeps one edge per `(sourceId, targetId, predicate)`, refuses a conflicting repeat or an undeclared endpoint, and `neighbors`, `walk`, and `isReachable` query it by predicate and direction; `KnowledgeRelationGraphSchema` round-trips a persisted graph with its metadata.

## Prove what the agent saw and used

A page existing in a knowledge base, a page appearing in retrieval results, and a page influencing a decision are three different facts. The receipt APIs preserve those joins without pretending they prove the page is true or that it improved the outcome.
Expand Down
20 changes: 20 additions & 0 deletions api-surface.json
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,7 @@
"AuditKnowledgeCitationsOptions": "value",
"BuildAgentMemorySequencesFromBenchmarkCasesOptions": "type",
"BuildEvalKnowledgeBundleOptions": "value",
"BuildKnowledgeRelationGraphInput": "value",
"BuildRetrievalBenchmarkCasesFromQrelsOptions": "type",
"BuildRetrievalEvalDispatchOptions": "value",
"CHECKABLE_RUNG_THRESHOLD": "value",
Expand Down Expand Up @@ -240,6 +241,8 @@
"KnowledgePage": "value",
"KnowledgePageInvalidation": "value",
"KnowledgePageInvalidationSchema": "value",
"KnowledgePageRelation": "value",
"KnowledgePageRelationPredicate": "value",
"KnowledgePageSchema": "value",
"KnowledgePolicy": "value",
"KnowledgePolicyDispatch": "type",
Expand All @@ -249,6 +252,18 @@
"KnowledgeReadinessSpec": "value",
"KnowledgeReceiptAttributeValue": "value",
"KnowledgeRelation": "value",
"KnowledgeRelationDirection": "value",
"KnowledgeRelationGraph": "value",
"KnowledgeRelationGraphError": "value",
"KnowledgeRelationGraphErrorCode": "value",
"KnowledgeRelationGraphSchema": "value",
"KnowledgeRelationNeighbor": "value",
"KnowledgeRelationNode": "value",
"KnowledgeRelationNodeSchema": "value",
"KnowledgeRelationQuery": "value",
"KnowledgeRelationSchema": "value",
"KnowledgeRelationWalkOptions": "value",
"KnowledgeRelationWalkStep": "value",
"KnowledgeRelease": "value",
"KnowledgeReleaseInput": "value",
"KnowledgeReleaseReport": "value",
Expand Down Expand Up @@ -488,6 +503,7 @@
"buildKnowledgeBenchmarkScenarios": "value",
"buildKnowledgeGraph": "value",
"buildKnowledgeIndex": "value",
"buildKnowledgeRelationGraph": "value",
"buildRetrievalBenchmarkCasesFromQrels": "value",
"buildRetrievalEvalDispatch": "value",
"calibrateRagAnswerJudge": "value",
Expand Down Expand Up @@ -566,6 +582,7 @@
"isKernelAnchoredPath": "value",
"isKnowledgeMemoryBenchmarkCase": "value",
"isMissingFile": "value",
"isReachable": "value",
"isSafeKnowledgePath": "value",
"isScaffoldPath": "value",
"jsonCandidateCodec": "value",
Expand All @@ -577,6 +594,7 @@
"knowledgeImprovementRunDir": "value",
"knowledgeImprovementRunId": "value",
"knowledgePageDigest": "value",
"knowledgePageRelations": "value",
"knowledgeReleaseReport": "value",
"layoutFor": "value",
"lensDistribution": "value",
Expand All @@ -600,6 +618,7 @@
"memoryWriteResultToSourceRecord": "value",
"mergeClaimLedgers": "value",
"mergeTrackedClaims": "value",
"neighbors": "value",
"normalizeClaimText": "value",
"normalizeExternalRagScores": "value",
"normalizeLinkTarget": "value",
Expand Down Expand Up @@ -686,6 +705,7 @@
"verifyGradeableEvidence": "value",
"verifyKnowledgeRetrievalReceipt": "value",
"verifyKnowledgeUseReceipt": "value",
"walk": "value",
"withCitedClaim": "value",
"withKnowledgeImprovementCandidate": "value",
"withKnowledgeImprovementComparison": "value",
Expand Down
1 change: 1 addition & 0 deletions docs/architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ It owns the small set of primitives every serious agent knowledge system needs:
- generated knowledge pages and units
- claims with source references
- deterministic indexing, graph construction, search, and lint
- labeled relation graphs with one edge per `(source, target, predicate)` and neighbor, walk, and reachability queries
- retrieval/RAG candidate surfaces, gold-target scoring, and eval-loop adapters
- safe LLM write proposals
- eval-gated release confidence through `@tangle-network/agent-eval`
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@tangle-network/agent-knowledge",
"version": "8.0.10",
"version": "8.1.0",
"description": "Build, search, evaluate, and improve source-backed knowledge bases.",
"homepage": "https://github.com/tangle-network/agent-knowledge#readme",
"repository": {
Expand Down
233 changes: 233 additions & 0 deletions src/graph.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,233 @@
import { describe, expect, it } from 'vitest'
import { buildKnowledgeGraph, knowledgePageRelations } from './graph'
import { buildKnowledgeRelationGraph, neighbors } from './relation-graph'
import type { KnowledgeGraph, KnowledgePage } from './types'

function page(
input: Partial<KnowledgePage> & Pick<KnowledgePage, 'id' | 'title' | 'path'>,
): KnowledgePage {
return {
text: `${input.title} body`,
frontmatter: {},
sourceIds: [],
tags: [],
outLinks: [],
...input,
}
}

// Covers: a link and a citation that resolve to the same page twice each, a
// missing link target, a self link, an origin-qualified citation, a malformed
// citation, an ambiguous duplicate id, a wikilink resolved through a path
// basename, and a `contradicts` entry at every resolution outcome.
const pages: KnowledgePage[] = [
page({
id: 'attention',
title: 'Attention',
path: 'knowledge/concepts/attention.md',
sourceIds: ['s1', 's2'],
tags: ['transformer'],
outLinks: ['flash-attention', 'Flash Attention', 'missing-page', 'attention'],
cites: ['flash-attention', 'here::flash-attention'],
contradicts: ['orphan'],
}),
page({
id: 'flash-attention',
title: 'Flash Attention',
path: 'knowledge/concepts/flash-attention.md',
sourceIds: ['s1'],
outLinks: ['attention'],
}),
page({ id: 'orphan', title: 'Orphan', path: 'knowledge/orphan.md', sourceIds: ['s2', 's3'] }),
page({ id: 'dup', title: 'Dup A', path: 'knowledge/dup-a.md', sourceIds: ['s3'] }),
page({ id: 'dup', title: 'Dup B', path: 'knowledge/dup-b.md', sourceIds: ['s3'] }),
page({
id: 'citer',
title: 'Citer',
path: 'knowledge/citer.md',
outLinks: ['dup-a', 'orphan.md'],
cites: ['dup', 'citer', 'here::', 'orphan'],
contradicts: ['citer', 'dup', 'orphan'],
}),
]

// Recorded from `buildKnowledgeGraph` before it derived its edges from
// `knowledgePageRelations`. Key order, edge order, weights, and degrees are the
// published index shape and must not move.
const goldenGraph: KnowledgeGraph = {
nodes: [
{
id: 'attention',
title: 'Attention',
path: 'knowledge/concepts/attention.md',
tags: ['transformer'],
sourceIds: ['s1', 's2'],
outDegree: 4,
inDegree: 1,
},
{
id: 'flash-attention',
title: 'Flash Attention',
path: 'knowledge/concepts/flash-attention.md',
tags: [],
sourceIds: ['s1'],
outDegree: 1,
inDegree: 4,
},
{
id: 'orphan',
title: 'Orphan',
path: 'knowledge/orphan.md',
tags: [],
sourceIds: ['s2', 's3'],
outDegree: 0,
inDegree: 2,
},
{
id: 'dup',
title: 'Dup A',
path: 'knowledge/dup-a.md',
tags: [],
sourceIds: ['s3'],
outDegree: 0,
inDegree: 1,
},
{
id: 'dup',
title: 'Dup B',
path: 'knowledge/dup-b.md',
tags: [],
sourceIds: ['s3'],
outDegree: 0,
inDegree: 1,
},
{
id: 'citer',
title: 'Citer',
path: 'knowledge/citer.md',
tags: [],
sourceIds: [],
outDegree: 3,
inDegree: 0,
},
],
edges: [
{
source: 'attention',
target: 'flash-attention',
weight: 4.5,
reasons: ['wikilink', 'citation', 'shared-source'],
},
{ source: 'citer', target: 'orphan', weight: 2, reasons: ['wikilink', 'citation'] },
{ source: 'flash-attention', target: 'attention', weight: 1, reasons: ['wikilink'] },
{ source: 'citer', target: 'dup', weight: 1, reasons: ['wikilink'] },
{ source: 'orphan', target: 'dup', weight: 1, reasons: ['shared-source'] },
{ source: 'attention', target: 'orphan', weight: 0.5, reasons: ['shared-source'] },
{ source: 'dup', target: 'dup', weight: 0.5, reasons: ['shared-source'] },
],
}

describe('knowledgePageRelations', () => {
it('emits one labeled relation per (source, target, predicate) with occurrence weights', () => {
expect(knowledgePageRelations(pages)).toEqual([
{ sourceId: 'attention', targetId: 'flash-attention', predicate: 'wikilink', weight: 2 },
{ sourceId: 'attention', targetId: 'flash-attention', predicate: 'citation', weight: 2 },
{ sourceId: 'attention', targetId: 'orphan', predicate: 'contradicts', weight: 1 },
{ sourceId: 'flash-attention', targetId: 'attention', predicate: 'wikilink', weight: 1 },
{ sourceId: 'citer', targetId: 'dup', predicate: 'wikilink', weight: 1 },
{ sourceId: 'citer', targetId: 'orphan', predicate: 'wikilink', weight: 1 },
{ sourceId: 'citer', targetId: 'orphan', predicate: 'citation', weight: 1 },
{ sourceId: 'citer', targetId: 'orphan', predicate: 'contradicts', weight: 1 },
{
sourceId: 'attention',
targetId: 'flash-attention',
predicate: 'shared-source',
weight: 0.5,
metadata: { sourceIds: ['s1'] },
},
{
sourceId: 'attention',
targetId: 'orphan',
predicate: 'shared-source',
weight: 0.5,
metadata: { sourceIds: ['s2'] },
},
{
sourceId: 'orphan',
targetId: 'dup',
predicate: 'shared-source',
weight: 0.5,
metadata: { sourceIds: ['s3'] },
},
{
sourceId: 'orphan',
targetId: 'dup',
predicate: 'shared-source',
weight: 0.5,
metadata: { sourceIds: ['s3'] },
},
{
sourceId: 'dup',
targetId: 'dup',
predicate: 'shared-source',
weight: 0.5,
metadata: { sourceIds: ['s3'] },
},
])
})

it('keeps two predicates between one pair as two relations in a relation graph', () => {
const relations = knowledgePageRelations(pages).filter(
(relation) => relation.predicate !== 'shared-source',
)
const graph = buildKnowledgeRelationGraph({ relations })
const out = neighbors(graph, 'attention', { direction: 'out' })
expect(out.map((neighbor) => [neighbor.nodeId, neighbor.relation.predicate])).toEqual([
['flash-attention', 'wikilink'],
['flash-attention', 'citation'],
['orphan', 'contradicts'],
])
expect(neighbors(graph, 'orphan', { direction: 'in', predicate: 'contradicts' })).toHaveLength(
2,
)
})

it('emits nothing for a missing, ambiguous, or self target', () => {
const relations = knowledgePageRelations(pages)
expect(relations.some((relation) => relation.targetId === 'missing-page')).toBe(false)
expect(
relations.some(
(relation) =>
relation.sourceId === relation.targetId && relation.predicate !== 'shared-source',
),
).toBe(false)
expect(
relations.some(
(relation) =>
relation.sourceId === 'citer' &&
relation.targetId === 'dup' &&
relation.predicate !== 'wikilink',
),
).toBe(false)
})
})

describe('buildKnowledgeGraph', () => {
it('is byte-identical to the recorded collapsed page graph', () => {
expect(JSON.stringify(buildKnowledgeGraph(pages))).toBe(JSON.stringify(goldenGraph))
})

it('sums the relation weights per ordered pair and leaves contradicts out', () => {
const graph = buildKnowledgeGraph(pages)
const pairWeights = new Map<string, number>()
for (const relation of knowledgePageRelations(pages)) {
if (relation.predicate === 'contradicts') continue
const key = `${relation.sourceId}->${relation.targetId}`
pairWeights.set(key, (pairWeights.get(key) ?? 0) + relation.weight)
}
expect(
new Map(graph.edges.map((edge) => [`${edge.source}->${edge.target}`, edge.weight])),
).toEqual(pairWeights)
expect(graph.edges.some((edge) => edge.reasons.includes('contradicts'))).toBe(false)
})
})
Loading