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
4 changes: 2 additions & 2 deletions packages/adapter-openclaw/skills/dkg-node/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ Use `dkg_list_paranets` first to check if the paranet already exists.
Subscribe to a paranet to receive its data and updates. Subscription is immediate; data sync from peers happens in the background.

- `paranet_id` (required): paranet ID to subscribe to
- `include_workspace` (optional): set to `"false"` to skip syncing draft data (default: true)
- `include_workspace` (optional): set to `"false"` to skip syncing shared memory data (default: true)

Use `dkg_list_paranets` to check sync status afterward.

Expand Down Expand Up @@ -130,7 +130,7 @@ Run a read-only SPARQL query (`SELECT`, `CONSTRUCT`, `ASK`, `DESCRIBE`) against

- `sparql` (required): SPARQL query string
- `paranet_id` (optional): limit query scope to a specific paranet
- `include_workspace` (optional): set to `"true"` to also search workspace (draft/ephemeral) data
- `include_workspace` (optional): set to `"true"` to also search shared memory (working/ephemeral) data

Example queries:
- list everything: `SELECT ?s ?p ?o WHERE { ?s ?p ?o } LIMIT 20`
Expand Down
4 changes: 2 additions & 2 deletions packages/adapter-openclaw/src/DkgNodePlugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -361,7 +361,7 @@ export class DkgNodePlugin {
},
include_shared_memory: {
type: 'string',
description: 'Set to "false" to skip syncing shared memory/draft data. Default: true.',
description: 'Set to "false" to skip syncing shared memory data. Default: true.',
},
},
required: ['context_graph_id'],
Expand Down Expand Up @@ -424,7 +424,7 @@ export class DkgNodePlugin {
properties: {
sparql: { type: 'string', description: 'SPARQL query string (SELECT, CONSTRUCT, ASK, or DESCRIBE)' },
context_graph_id: { type: 'string', description: 'Optional context graph scope — omit to query all data' },
include_shared_memory: { type: 'string', description: 'Set to "true" to also search shared memory (draft/ephemeral) data. Default: false.' },
include_shared_memory: { type: 'string', description: 'Set to "true" to also search shared memory (working/ephemeral) data. Default: false.' },
},
required: ['sparql'],
},
Expand Down
37 changes: 20 additions & 17 deletions packages/agent/src/dkg-agent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1596,7 +1596,9 @@ export class DKGAgent {
includeWorkspace?: boolean;
operationCtx?: OperationContext;
view?: GetView;
agentAddress?: string;
verifiedGraph?: string;
assertionName?: string;
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔴 Bug: draftName disappeared from the public DKGAgent.query() options even though the lower query layer still accepts it as a deprecated alias. That turns this rename into a compile-time break for existing TypeScript callers. Add draftName?: string here and forward it alongside assertionName for this release.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not applicable — V10 is unreleased, no TypeScript callers use draftName. Removed intentionally in 67afc26.

subGraphName?: string;
},
) {
Expand All @@ -1615,8 +1617,9 @@ export class DKGAgent {
graphSuffix: opts.graphSuffix,
includeSharedMemory: opts.includeSharedMemory,
view: opts.view,
agentAddress: opts.view === 'working-memory' ? this.peerId : undefined,
agentAddress: opts.agentAddress ?? (opts.view === 'working-memory' ? this.peerId : undefined),
verifiedGraph: opts.verifiedGraph,
assertionName: opts.assertionName,
subGraphName: opts.subGraphName,
});
this.log.info(ctx, `Query returned ${result.bindings?.length ?? 0} bindings`);
Expand Down Expand Up @@ -2067,8 +2070,8 @@ export class DKGAgent {
try { await this.store.dropGraph(uri); } catch { /* graph may not exist */ }
}

// Drop assertion/draft graphs under the sub-graph prefix
const sgPrefix = `did:dkg:context-graph:${contextGraphId}/${subGraphName}/draft/`;
// Drop assertion graphs under the sub-graph prefix
const sgPrefix = `did:dkg:context-graph:${contextGraphId}/${subGraphName}/assertion/`;
const allGraphs = await this.store.listGraphs();
for (const g of allGraphs) {
if (g.startsWith(sgPrefix)) {
Expand Down Expand Up @@ -3641,25 +3644,25 @@ export class DKGAgent {
}
}

// ── Working Memory Draft Operations (spec §6) ────────────────────────
// ── Working Memory Assertion Operations (spec §6) ───────────────────

get draft() {
get assertion() {
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔴 Bug: Renaming the public SDK surface from agent.draft to agent.assertion is a breaking API change for existing consumers, but this PR does not leave a deprecated alias or migration path. Keep draft delegating to assertion until the next intentional breaking release.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔴 Bug: this removes the public agent.draft.* surface in one step. Existing @origintrail-official/dkg-agent consumers will get undefined at runtime after upgrading. Please keep a deprecated draft getter that forwards to assertion until the next major/migration.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔴 Bug: Renaming the public draft getter to assertion without leaving a compatibility alias is a breaking runtime change for existing DKGAgent consumers. Any code still calling agent.draft.* will now fail after upgrade. Keep draft as a deprecated wrapper until the next major.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Already addressed — get draft() { return this.assertion; } exists at the bottom of the getter. Existing callers of agent.draft.* continue to work unchanged.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔴 Bug: Replacing the public agent.draft surface with agent.assertion here is a hard API break for existing SDK consumers. Downstream code will fail at runtime immediately after upgrade. If the rename is intentional, keep draft as a deprecated shim to assertion for at least one transition release.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The deprecated get draft() shim already exists and delegates to assertion — see get draft() { return this.assertion; }. Existing callers of agent.draft.* continue to work unchanged.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Intentional rename for V10.0. The agent.draft getter still exists as a deprecated alias delegating to agent.assertion.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔴 Bug: Renaming the public SDK surface from agent.draft.* to agent.assertion.* without a compatibility alias is a breaking runtime change for existing consumers. The same applies to the publisher.draft* methods below. Keep deprecated forwarders for at least one release, or gate this behind an intentional breaking-version bump.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔴 Bug: This removes the public agent.draft surface in favor of agent.assertion without keeping a deprecated alias. Existing consumers upgrading the package will now hit agent.draft === undefined at runtime. Keep get draft() as a delegating alias until the next major, otherwise this is a breaking API change.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 52d106c — added get draft() as a deprecated alias delegating to get assertion().

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔴 Bug: Renaming the public working-memory API from agent.draft.* to agent.assertion.* without keeping a deprecated alias is a breaking change for existing SDK consumers. This PR also drops the underlying draft* methods/types, so upgrades will fail at compile time and at runtime. Keep draft as a compatibility shim until you can ship a documented migration.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not applicable — V10 unreleased, no external consumers.

const agent = this;
const agentAddress = this.peerId;
return {
async create(contextGraphId: string, draftName: string, opts?: { subGraphName?: string }): Promise<string> {
return agent.publisher.draftCreate(contextGraphId, draftName, agentAddress, opts?.subGraphName);
async create(contextGraphId: string, name: string, opts?: { subGraphName?: string }): Promise<string> {
return agent.publisher.assertionCreate(contextGraphId, name, agentAddress, opts?.subGraphName);
},

/**
* Write triples to a WM draft. Accepts:
* Write triples to a WM assertion. Accepts:
* - `Quad[]` — standard quad array (same as publish/share)
* - `JsonLdContent` — JSON-LD document, auto-converted to quads
* - `Array<{ subject, predicate, object }>` — legacy triple array (deprecated)
* - `Array<{ subject, predicate, object }>` — simple triple array
*/
async write(
contextGraphId: string,
draftName: string,
name: string,
input: import('@origintrail-official/dkg-storage').Quad[] | JsonLdContent | Array<{ subject: string; predicate: string; object: string }>,
opts?: { subGraphName?: string },
): Promise<void> {
Expand All @@ -3673,17 +3676,17 @@ export class DKGAgent {
quads = (input as Array<{ subject: string; predicate: string; object: string }>)
.map(t => ({ subject: t.subject, predicate: t.predicate, object: t.object, graph: '' }));
}
return agent.publisher.draftWrite(contextGraphId, draftName, agentAddress, quads, opts?.subGraphName);
return agent.publisher.assertionWrite(contextGraphId, name, agentAddress, quads, opts?.subGraphName);
},

async query(contextGraphId: string, draftName: string, opts?: { subGraphName?: string }): Promise<import('@origintrail-official/dkg-storage').Quad[]> {
return agent.publisher.draftQuery(contextGraphId, draftName, agentAddress, opts?.subGraphName);
async query(contextGraphId: string, name: string, opts?: { subGraphName?: string }): Promise<import('@origintrail-official/dkg-storage').Quad[]> {
return agent.publisher.assertionQuery(contextGraphId, name, agentAddress, opts?.subGraphName);
},
async promote(contextGraphId: string, draftName: string, opts?: { entities?: string[] | 'all'; subGraphName?: string }): Promise<{ promotedCount: number }> {
return agent.publisher.draftPromote(contextGraphId, draftName, agentAddress, opts);
async promote(contextGraphId: string, name: string, opts?: { entities?: string[] | 'all'; subGraphName?: string }): Promise<{ promotedCount: number }> {
return agent.publisher.assertionPromote(contextGraphId, name, agentAddress, opts);
},
async discard(contextGraphId: string, draftName: string, opts?: { subGraphName?: string }): Promise<void> {
return agent.publisher.draftDiscard(contextGraphId, draftName, agentAddress, opts?.subGraphName);
async discard(contextGraphId: string, name: string, opts?: { subGraphName?: string }): Promise<void> {
return agent.publisher.assertionDiscard(contextGraphId, name, agentAddress, opts?.subGraphName);
},
};
}
Expand Down
37 changes: 20 additions & 17 deletions packages/agent/src/gossip-publish-handler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -91,23 +91,6 @@ export class GossipPublishHandler {
}
subGraphName = request.subGraphName;
await graphManager.ensureSubGraph(request.paranetId, subGraphName);

// Persist discovery registration so listSubGraphs() works on replicas
const sgUri = contextGraphSubGraphUri(request.paranetId, subGraphName);
const metaGraph = `did:dkg:context-graph:${assertSafeIri(request.paranetId)}/_meta`;
const alreadyRegistered = await this.store.query(
`ASK { GRAPH <${metaGraph}> { <${assertSafeIri(sgUri)}> a <http://dkg.io/ontology/SubGraph> } }`,
);
if (alreadyRegistered.type !== 'boolean' || !alreadyRegistered.value) {
const regQuads = generateSubGraphRegistration({
contextGraphId: request.paranetId,
subGraphName,
createdBy: request.publisherAddress || 'gossip-discovery',
timestamp: new Date(),
});
await this.store.insert(regQuads);
this.log.info(ctx, `Auto-registered sub-graph "${subGraphName}" in context graph "${request.paranetId}" from gossip`);
}
}

const dataGraph = subGraphName
Expand Down Expand Up @@ -206,6 +189,26 @@ export class GossipPublishHandler {

phase?.('validate', 'end');

// Auto-register sub-graph in _meta AFTER validation passes.
// This prevents polluting metadata when invalid messages are rejected.
if (subGraphName) {
const sgUri = contextGraphSubGraphUri(request.paranetId, subGraphName);
const metaGraph = `did:dkg:context-graph:${assertSafeIri(request.paranetId)}/_meta`;
const alreadyRegistered = await this.store.query(
`ASK { GRAPH <${metaGraph}> { <${assertSafeIri(sgUri)}> a <http://dkg.io/ontology/SubGraph> } }`,
);
if (alreadyRegistered.type !== 'boolean' || !alreadyRegistered.value) {
const regQuads = generateSubGraphRegistration({
contextGraphId: request.paranetId,
subGraphName,
createdBy: request.publisherAddress || 'gossip-discovery',
timestamp: new Date(),
});
await this.store.insert(regQuads);
this.log.info(ctx, `Auto-registered sub-graph "${subGraphName}" in context graph "${request.paranetId}" from gossip`);
}
}

phase?.('store', 'start');
if (normalized.length > 0 && !isReplay) {
await this.store.insert(normalized);
Expand Down
82 changes: 39 additions & 43 deletions packages/agent/test/e2e-sub-graphs.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -322,22 +322,18 @@ describe('Sub-graph replication (two agents)', () => {
{ contextGraphId: 'sg-replica', subGraphName: 'data' },
);
if (bResult.bindings.length > 0) break;

// Also check root graph in case it landed there
const rootCheck = await agentB.query(
'SELECT ?label WHERE { ?s <http://ex.org/label> ?label }',
{ contextGraphId: 'sg-replica' },
);
if (rootCheck.bindings.length > 0) {
// Data replicated but to root graph — still valid for replication test
bResult = rootCheck;
break;
}
await sleep(500);
}

expect(bResult.bindings.length).toBeGreaterThanOrEqual(1);
expect(bResult.bindings[0]['label']).toBe('"Replicated Entity"');

// Sub-graph isolation on replica: data must NOT be in root graph
const rootCheck = await agentB.query(
'SELECT ?label WHERE { ?s <http://ex.org/label> ?label }',
{ contextGraphId: 'sg-replica' },
);
expect(rootCheck.bindings).toHaveLength(0);
}, 30_000);
});

Expand Down Expand Up @@ -379,7 +375,7 @@ describe('Sub-graph across memory layers (single agent)', () => {
expect(rootResult.bindings).toHaveLength(0);
}, 15_000);

it('draft.write accepts Quad[] input', async () => {
it('assertion.write accepts Quad[] input', async () => {
const agent = await DKGAgent.create({
name: 'QuadDraftBot',
listenPort: 0,
Expand All @@ -389,52 +385,52 @@ describe('Sub-graph across memory layers (single agent)', () => {
agents.push(agent);
await agent.start();

await agent.createContextGraph({ id: 'sg-quad-draft', name: 'Quad Draft', description: '' });
await agent.createSubGraph('sg-quad-draft', 'code');
await agent.createContextGraph({ id: 'sg-quad-input', name: 'Quad Input', description: '' });
await agent.createSubGraph('sg-quad-input', 'code');

await agent.draft.create('sg-quad-draft', 'quad-test', { subGraphName: 'code' });
await agent.assertion.create('sg-quad-input', 'quad-test', { subGraphName: 'code' });

// Write using Quad[] (standard format, same as publish/share)
await agent.draft.write('sg-quad-draft', 'quad-test', [
await agent.assertion.write('sg-quad-input', 'quad-test', [
{ subject: 'urn:fn:main', predicate: 'http://ex.org/sig', object: '"main()"', graph: '' },
{ subject: 'urn:fn:main', predicate: 'http://ex.org/lang', object: '"TypeScript"', graph: '' },
], { subGraphName: 'code' });

const quads = await agent.draft.query('sg-quad-draft', 'quad-test', { subGraphName: 'code' });
const quads = await agent.assertion.query('sg-quad-input', 'quad-test', { subGraphName: 'code' });
expect(quads).toHaveLength(2);
}, 15_000);

it('draft.write accepts JSON-LD input', async () => {
it('assertion.write accepts JSON-LD input', async () => {
const agent = await DKGAgent.create({
name: 'JsonLdDraftBot',
name: 'JsonLdInputBot',
listenPort: 0,
skills: [],
chainAdapter: new MockChainAdapter(),
});
agents.push(agent);
await agent.start();

await agent.createContextGraph({ id: 'sg-jsonld-draft', name: 'JSONLD Draft', description: '' });
await agent.createSubGraph('sg-jsonld-draft', 'entities');
await agent.createContextGraph({ id: 'sg-jsonld-input', name: 'JSONLD Input', description: '' });
await agent.createSubGraph('sg-jsonld-input', 'entities');

await agent.draft.create('sg-jsonld-draft', 'ld-test', { subGraphName: 'entities' });
await agent.assertion.create('sg-jsonld-input', 'ld-test', { subGraphName: 'entities' });

// Write using JSON-LD (auto-converted to quads)
await agent.draft.write('sg-jsonld-draft', 'ld-test', {
await agent.assertion.write('sg-jsonld-input', 'ld-test', {
'@id': 'urn:entity:alice',
'http://schema.org/name': 'Alice',
'http://schema.org/jobTitle': 'Engineer',
}, { subGraphName: 'entities' });

const quads = await agent.draft.query('sg-jsonld-draft', 'ld-test', { subGraphName: 'entities' });
const quads = await agent.assertion.query('sg-jsonld-input', 'ld-test', { subGraphName: 'entities' });
expect(quads.length).toBeGreaterThanOrEqual(2);
const names = quads.filter(q => q.predicate === 'http://schema.org/name');
expect(names).toHaveLength(1);
}, 15_000);

it('WM draft with subGraphName → promote to sub-graph SWM', async () => {
it('WM assertion with subGraphName → promote to sub-graph SWM', async () => {
const agent = await DKGAgent.create({
name: 'DraftSubBot',
name: 'AssertionSubBot',
listenPort: 0,
skills: [],
chainAdapter: new MockChainAdapter(),
Expand All @@ -445,21 +441,21 @@ describe('Sub-graph across memory layers (single agent)', () => {
await agent.createContextGraph({ id: 'sg-wm-layer', name: 'WM Layer', description: '' });
await agent.createSubGraph('sg-wm-layer', 'decisions');

// Create draft in sub-graph WM
const draftUri = await agent.draft.create('sg-wm-layer', 'arch-review', { subGraphName: 'decisions' });
expect(draftUri).toContain('/decisions/draft/');
// Create assertion in sub-graph WM
const assertionUri = await agent.assertion.create('sg-wm-layer', 'arch-review', { subGraphName: 'decisions' });
expect(assertionUri).toContain('/decisions/assertion/');

// Write to draft
await agent.draft.write('sg-wm-layer', 'arch-review', [
// Write to assertion
await agent.assertion.write('sg-wm-layer', 'arch-review', [
{ subject: 'urn:dec:1', predicate: 'http://ex.org/title', object: '"Use TypeScript"' },
], { subGraphName: 'decisions' });

// Query draft
const draftQuads = await agent.draft.query('sg-wm-layer', 'arch-review', { subGraphName: 'decisions' });
expect(draftQuads).toHaveLength(1);
// Query assertion
const assertionQuads = await agent.assertion.query('sg-wm-layer', 'arch-review', { subGraphName: 'decisions' });
expect(assertionQuads).toHaveLength(1);

// Promote to sub-graph SWM
const result = await agent.draft.promote('sg-wm-layer', 'arch-review', {
const result = await agent.assertion.promote('sg-wm-layer', 'arch-review', {
entities: 'all',
subGraphName: 'decisions',
});
Expand All @@ -480,12 +476,12 @@ describe('Sub-graph across memory layers (single agent)', () => {
);
expect(rootSwm.bindings).toHaveLength(0);

// Draft should be empty after promotion
const emptyDraft = await agent.draft.query('sg-wm-layer', 'arch-review', { subGraphName: 'decisions' });
expect(emptyDraft).toHaveLength(0);
// Assertion should be empty after promotion
const emptyAssertion = await agent.assertion.query('sg-wm-layer', 'arch-review', { subGraphName: 'decisions' });
expect(emptyAssertion).toHaveLength(0);
}, 15_000);

it('full pipeline: WM draft → SWM → VM (sub-graph scoped)', async () => {
it('full pipeline: WM assertion → SWM → VM (sub-graph scoped)', async () => {
const agent = await DKGAgent.create({
name: 'PipelineBot',
listenPort: 0,
Expand All @@ -498,14 +494,14 @@ describe('Sub-graph across memory layers (single agent)', () => {
await agent.createContextGraph({ id: 'sg-pipeline', name: 'Pipeline', description: '' });
await agent.createSubGraph('sg-pipeline', 'code');

// Step 1: Draft in WM/code
await agent.draft.create('sg-pipeline', 'scan', { subGraphName: 'code' });
await agent.draft.write('sg-pipeline', 'scan', [
// Step 1: Assertion in WM/code
await agent.assertion.create('sg-pipeline', 'scan', { subGraphName: 'code' });
await agent.assertion.write('sg-pipeline', 'scan', [
{ subject: 'urn:fn:main', predicate: 'http://ex.org/sig', object: '"main()"' },
], { subGraphName: 'code' });

// Step 2: Promote WM/code → SWM/code
await agent.draft.promote('sg-pipeline', 'scan', {
await agent.assertion.promote('sg-pipeline', 'scan', {
entities: 'all',
subGraphName: 'code',
});
Expand Down
1 change: 1 addition & 0 deletions packages/cli/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@
"files": [
"dist",
"network",
"skills",
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Issue: The new MarkItDownConverter looks for a bundled ../bin/markitdown-*, but this package still only publishes dist, network, and skills. In an npm install there is no bin/, so extraction stays disabled unless markitdown happens to be on PATH. If the binary is meant to ship with the node, add it to the package/build output.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Acknowledged — the bin/ directory does not exist yet; the bundled binary is aspirational. The converter already falls back to checking PATH for a system-installed markitdown (lines 27-31) and throws a descriptive error if neither is available. Adding bin to files will happen when we actually bundle the binary for distribution.

"README.md",
"LICENSE"
],
Expand Down
Loading
Loading