Merging Agent Results
This tutorial continues from Branching for Agent Isolation.
Several agents have worked on their own branches; now you review their output,
merge what is good, handle a conflict, and roll main back when a merge turns
out to be wrong.
What you'll learn
- Reviewing an agent branch before merging
- Merging one branch, then several in sequence
- Promoting only selected results
- What a conflict looks like and how to complete the merge
- Rolling
mainback after a bad merge
Setup
Two agents worked from the same main, each on its own branch:
const db = client.database('research');
await db.branches().create('agent/researcher', { fromBranch: 'main' });
await db.branches().create('agent/fact-checker', { fromBranch: 'main' });
const researcher = db.onBranch('agent/researcher').workspace('findings').nodes();
await researcher.create({ type: 'raisin:Page', path: '/q1/market-analysis',
properties: { title: 'Market Analysis Q1', confidence: 0.92 } });
await researcher.create({ type: 'raisin:Page', path: '/q1/competitors',
properties: { title: 'Competitor Landscape', confidence: 0.61 } });
const checker = db.onBranch('agent/fact-checker').workspace('findings').nodes();
await checker.create({ type: 'raisin:Page', path: '/q1/verified-sources',
properties: { title: 'Verified sources', confidence: 0.99 } });
Step 1: Review a branch
Read the branch like any other database, and ask for its diff against main:
const r = db.onBranch('agent/researcher');
await r.executeSql("SELECT path, properties->>'confidence' AS confidence FROM 'findings' WHERE node_type = 'raisin:Page' ORDER BY path");
// rows: [{ path: '/q1/competitors', confidence: '0.61' }, { path: '/q1/market-analysis', confidence: '0.92' }]
await db.branches().diff('agent/researcher', 'main');
// { added: [{ path: '/q1/market-analysis', ... }, { path: '/q1/competitors', ... }], modified: [], deleted: [] }
Step 2: Merge one branch
await db.branches().merge('agent/researcher', 'main', { message: 'Merge researcher findings' });
// { success: true, revision: 1788720003593, conflicts: [], fast_forward: false, nodes_changed: 2 }
await db.executeSql("SELECT path FROM 'findings' WHERE node_type = 'raisin:Page'");
// rows: [{ path: '/q1/market-analysis' }, { path: '/q1/competitors' }]
Step 3: Merge a second branch
The fact-checker touched different nodes, so its merge combines cleanly with
what is already on main:
await db.branches().merge('agent/fact-checker', 'main', { message: 'Merge fact-checker' });
// { success: true, ..., nodes_changed: 1 }
Merging several agents is a sequence of merges into main; each merge sees
the result of the previous one.
Step 4: Promote only the good results
Suppose the low-confidence competitor page should not have gone live. Instead
of merging a whole branch, copy the roots you want with copyNodes. Ids are
preserved, so re-promoting later updates the same nodes:
await db.branches().create('agent/summarizer', { fromBranch: 'main' });
const s = db.onBranch('agent/summarizer').workspace('findings').nodes();
await s.create({ type: 'raisin:Page', path: '/q1/summary', properties: { title: 'Executive summary', confidence: 0.9 } });
await s.create({ type: 'raisin:Page', path: '/q1/scratch', properties: { title: 'Dead end', confidence: 0.2 } });
await db.branches().copyNodes('agent/summarizer', 'main', {
workspace: 'findings',
roots: ['/q1/summary'],
recursive: true,
});
// { copied: 1, deleted: 0, revision: '1788720003641-0', changes: [{ path: '/q1/summary', operation: 'added', ... }] }
/q1/scratch never reaches main. Drop the branch when done.
Step 5: Handle a conflict
Two agents that edit the same node produce a conflict. Fork a branch, edit
/q1/market-analysis on it, and edit the same node on main:
await db.branches().create('agent/reviser', { fromBranch: 'main' });
await db.onBranch('agent/reviser').executeSql(
"UPDATE 'findings' SET properties = '{\"title\":\"Market Analysis Q1\",\"confidence\":0.97}'::jsonb WHERE path = '/q1/market-analysis'");
await db.executeSql(
"UPDATE 'findings' SET properties = '{\"title\":\"Market Analysis Q1 (edited)\",\"confidence\":0.92}'::jsonb WHERE path = '/q1/market-analysis'");
const result = await db.branches().merge('agent/reviser', 'main', { message: 'Merge reviser' });
// { success: false, revision: null, fast_forward: false, nodes_changed: 0,
// conflicts: [{ node_id: '260584c1-...', conflict_type: 'BothModified',
// base_properties: { title: 'Market Analysis Q1', confidence: 0.92 },
// target_properties: { title: 'Market Analysis Q1 (edited)', confidence: 0.92 },
// source_properties: { title: 'Market Analysis Q1', confidence: 0.97 } }] }
Nothing was written. Complete the merge by posting one resolution per node to
the HTTP resolve-merge endpoint, choosing keep-ours, keep-theirs or
manual and supplying the final properties:
curl -X POST localhost:8080/api/management/repositories/default/research/branches/main/resolve-merge \
-H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
-d '{
"source_branch": "agent/reviser",
"resolutions": [{
"node_id": "260584c1-...",
"resolution_type": "manual",
"resolved_properties": { "title": "Market Analysis Q1 (edited)", "confidence": 0.97 }
}],
"message": "Merge reviser (resolved)",
"actor": "orchestrator"
}'
# {"success":true,"revision":1788719912532,"conflicts":[],"fast_forward":false,"nodes_changed":1}
Agents that write to disjoint paths never conflict, which is the usual way to run them in parallel.
Step 6: Roll main back
Every merge is a revision on main. If a merge turns out to be bad, move the
head to the revision before it. The merged revisions stay in history, so you
can move forward again later.
const log = await fetch(
'http://localhost:8080/api/management/repositories/default/research/revisions?branch=main&limit=5',
{ headers: { Authorization: `Bearer ${token}` } }).then(r => r.json());
// log.revisions[0] is the latest merge; log.revisions[0].parent is main before it
await db.branches().updateHead('main', log.revisions[0].parent);
Tag main before risky merges so the rollback target has a name:
const head = await db.branches().getHead('main');
await db.tags().create('before-agent-merges', head.revision);
Step 7: Clean up
for (const b of ['agent/researcher', 'agent/fact-checker', 'agent/summarizer', 'agent/reviser']) {
await db.branches().delete(b);
}