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
1 change: 1 addition & 0 deletions examples/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ TANGLE_API_KEY=... pnpm tsx examples/supervise/supervise.ts # 3. one function
| 5 | [`supervise/`](./supervise/) | The one-call headline: `supervise(profile, goal)` runs a full supervisor with everything defaulted. Needs `TANGLE_API_KEY`. |
| 6 | [`supervisor-loop/`](./supervisor-loop/) | The same supervisor over a real worker backend — cloud sandbox, local coding-CLI, or an MCP server — with the backend as the only knob you change. |
| 7 | [`delegate/`](./delegate/) | `delegate(intent)`: the supervisor writes and spawns a worker that does real work on disk, and the run only settles once the file it was asked to create actually exists. Needs `TANGLE_API_KEY`. |
| 7b | [`graphs/`](./graphs/) | **Agent graphs**: four topologies (peer review loop, best-of-N, watchdog steer, shot loop) each authored as ≤25 lines of plain data and run through `runGraph`, printing the edge ledger — every traversal, delivered or not — as the proof. Offline. |

## Benchmarking — score agents against a check

Expand Down
30 changes: 30 additions & 0 deletions examples/graphs/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
# graphs — agent topologies as plain data

Four runnable topologies for `runGraph` (the agent-graph layer over `supervise()`).
Each file's graph is a ≤25-line data literal — nodes are canonical `AgentProfile`s, edges are typed values carrying versioned registry directives — and each `main()` prints the EDGE LEDGER as the proof artifact: every traversal, its outcome (`delivered | stripped | empty | unpropagated`), its byte count, and the concrete worker it reached.

All four run offline at $0 (scripted driver brain + in-process leaf workers, in [`shared.ts`](./shared.ts) — the same seams the kernel's own graph tests use).

```bash
pnpm tsx examples/graphs/collaborates-review-loop.ts
pnpm tsx examples/graphs/best-of-n.ts
pnpm tsx examples/graphs/watchdog-steer.ts
pnpm tsx examples/graphs/shot-loop.ts
```

| Example | Topology | What the ledger proves |
|---|---|---|
| [`collaborates-review-loop.ts`](./collaborates-review-loop.ts) | root + implementer + reviewer; `analyzes` critique → reviewer, `analyzes` verdict → driver | Peer collaboration is MEDIATED: findings cross worker→worker only as a ledgered lens route (a direct worker-to-worker channel is not a first-class edge), and a route with no live target is `unpropagated`, never dropped. |
| [`best-of-n.ts`](./best-of-n.ts) | root + two candidate coder nodes, one `delegates` edge each, `maxLiveWorkers: 2` | Breadth is two edges in the data: exactly two delivered spawn traversals, winner decided by the deliverable. |
| [`watchdog-steer.ts`](./watchdog-steer.ts) | root + one builder with a live trace; shipped online detector panel (`watchTrace`) | Mid-run intervention: the detector fires while the worker runs, and the corrective steer lands as the delegates edge's second delivered traversal BEFORE settle. |
| [`shot-loop.ts`](./shot-loop.ts) | reviewer(root) ↔ coder; `delegates maxTraversals: 3`, `analyzes` verify → reviewer | The multishot loop as data: each shot and each verify report is one ledgered traversal, the shot budget lives on the edge, and the deliverable gates on the verdict. |

The offline proof for all four (exact ledger counts, outcomes, destinations) lives in `tests/examples/graph-topologies.test.ts`.

## Two ledger semantics worth knowing

- A mid-run steer increments its delegates edge's traversal count but is only CAP-CHECKED at
spawn time — each steer consumes future spawn budget on that edge, so `maxTraversals: 3`
means "3 shots" only on a steer-free edge.
- `workerId` on a ledger row is the DESTINATION for delegates/steer/routed-analyzes rows, but
the SOURCE worker for driver-destined finding rows.
85 changes: 85 additions & 0 deletions examples/graphs/best-of-n.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
/**
* best-of-n — breadth as a topology: one delegates edge per candidate node.
*
* Two coder nodes with distinct ids and distinct profiles hang off one root. The driver spawns
* BOTH in a single turn (`maxLiveWorkers: 2` admits them concurrently), awaits both settles, and
* the run keeps the winner — the candidate whose settle passed the deliverable. The edge ledger
* shows exactly two delivered spawn traversals, one per candidate edge: breadth is two edges in
* the data, not a fan-out helper in code.
*
* Fully offline (scripted brain + leaf seam). Run: pnpm tsx examples/graphs/best-of-n.ts
*/

import type { AgentProfile } from '@tangle-network/agent-interface'
import {
type AgentGraph,
promptHandle,
type RunGraphOptions,
runGraph,
} from '@tangle-network/agent-runtime/kernel'
import { leafSeam, printLedger, scriptedBrain } from './shared'

const brief = promptHandle('delegates/worker-brief/v1')

export function bestOfN(): { graph: AgentGraph; opts: RunGraphOptions } {
// ── The topology: plain data ──
const graph: AgentGraph = {
nodes: [
{ id: 'lead', profile: { name: 'lead', prompt: { systemPrompt: 'Keep the best.' } } },
{ id: 'coder-a', profile: { name: 'coder-a', prompt: { systemPrompt: 'Minimal diff.' } } },
{ id: 'coder-b', profile: { name: 'coder-b', prompt: { systemPrompt: 'Full rewrite.' } } },
],
edges: [
{ kind: 'delegates', from: 'lead', to: 'coder-a', directive: brief },
{ kind: 'delegates', from: 'lead', to: 'coder-b', directive: brief },
],
deliverable: {
describe: 'a passing candidate',
check: (out) => (out as { pass?: boolean } | undefined)?.pass === true,
},
budget: { maxIterations: 30, maxTokens: 100_000 },
}

const received: AgentProfile[] = []
const opts: RunGraphOptions = {
runId: 'bon',
maxLiveWorkers: 2,
makeWorkerAgent: leafSeam(received, {
// Candidate A fails its check; candidate B passes — the pick is decided by outcome.
'coder-a': { shots: [{ out: { candidate: 'a', pass: false }, valid: false }] },
'coder-b': { shots: [{ out: { candidate: 'b', pass: true }, valid: true }] },
}),
brain: scriptedBrain([
{
// Both spawns in ONE driver turn — concurrent candidates under the conserved pool.
toolCalls: [
{
name: 'spawn_agent',
arguments: { profile: { name: 'coder-a' }, task: 'attempt the fix' },
},
{
name: 'spawn_agent',
arguments: { profile: { name: 'coder-b' }, task: 'attempt the fix' },
},
],
},
{ toolCalls: [{ name: 'await_event', arguments: {} }] },
{ toolCalls: [{ name: 'await_event', arguments: {} }] },
{ content: 'done' },
]),
}
return { graph, opts }
}

export async function main(): Promise<void> {
const { graph, opts } = bestOfN()
const res = await runGraph(graph, opts)
printLedger('best-of-n', res)
}

if (import.meta.url === `file://${process.argv[1]}`) {
main().catch((err) => {
console.error(err)
process.exit(1)
})
}
145 changes: 145 additions & 0 deletions examples/graphs/collaborates-review-loop.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
/**
* collaborates-review-loop — the PEER-COLLABORATION pattern expressible today.
*
* Two worker nodes under one root: an 'implementer' and a 'reviewer'. An analyzes edge (the
* 'critique' lens over the implementer's settle trace) routes findings to the REVIEWER as an
* authorized, ledgered steer; a second analyzes edge ('verdict' over the reviewer) routes the
* review verdict back to the DRIVER, which re-briefs the implementer with a second spawn.
*
* Say it plainly: a DIRECT worker-to-worker channel is not a first-class edge. Workers never
* address each other; what exists today is this MEDIATED form — findings travel worker → analyst
* lens → routed steer / driver re-brief — and every hop lands in the edge ledger, so nothing
* crosses between agents unobserved.
*
* The ledger this prints also shows the honest tail: when the re-briefed implementer settles,
* the critique lens fires again, but the reviewer has already settled — that traversal is
* ledgered `unpropagated` (unknown-worker), not silently dropped.
*
* Fully offline (scripted brain + leaf seam). Run: pnpm tsx examples/graphs/collaborates-review-loop.ts
*/

import type { AgentProfile } from '@tangle-network/agent-interface'
import {
type AgentGraph,
type AnalystRegistry,
promptHandle,
type RunGraphOptions,
runGraph,
} from '@tangle-network/agent-runtime/kernel'
import { leafSeam, printLedger, scriptedBrain } from './shared'

const brief = promptHandle('delegates/worker-brief/v1')
const report = promptHandle('analyzes/findings-report/v1')

/** The two lenses are ENVIRONMENT (registry entries), never nodes in the graph. */
const analysts: AnalystRegistry = {
kinds: [
{ id: 'critique', description: 'read the implementer trace, list defects', area: 'review' },
{ id: 'verdict', description: 'read the reviewer trace, extract the verdict', area: 'review' },
],
run: async (kindId) =>
kindId === 'critique'
? [{ claim: 'implementation lacks tests', severity: 'major' }]
: { verdict: 'needs-changes', brief: 'add the missing tests' },
}

export function collaboratesReviewLoop(): { graph: AgentGraph; opts: RunGraphOptions } {
// ── The topology: plain data ──
const graph: AgentGraph = {
nodes: [
{ id: 'driver', profile: { name: 'driver', prompt: { systemPrompt: 'Drive the loop.' } } },
{ id: 'implementer', profile: { name: 'implementer', prompt: { systemPrompt: 'Build.' } } },
{ id: 'reviewer', profile: { name: 'reviewer', prompt: { systemPrompt: 'Review.' } } },
],
edges: [
{ kind: 'delegates', from: 'driver', to: 'implementer', directive: brief },
{ kind: 'delegates', from: 'driver', to: 'reviewer', directive: brief },
{
kind: 'analyzes',
analyst: 'critique',
over: ['implementer'],
to: 'reviewer',
directive: report,
},
{ kind: 'analyzes', analyst: 'verdict', over: ['reviewer'], to: 'driver', directive: report },
],
deliverable: { describe: 'the re-briefed implementation', check: (out) => out !== undefined },
budget: { maxIterations: 40, maxTokens: 100_000 },
}

const received: AgentProfile[] = []
const opts: RunGraphOptions = {
runId: 'collab',
analysts,
makeWorkerAgent: leafSeam(received, {
// Shot 1 is the draft; shot 2 (after the driver's re-brief) is the revision that wins.
implementer: {
withTrace: true,
shots: [
{ out: { revision: 1 }, valid: true, score: 0.5 },
{ out: { revision: 2 }, valid: true, score: 1 },
],
},
// The reviewer stays LIVE until the routed critique steer arrives (that steer is what
// releases it), and exposes its own trace so the verdict lens can read it.
reviewer: {
awaitSteer: true,
withTrace: true,
shots: [{ out: { review: 'needs-changes' }, valid: true, score: 0.6 }],
},
}),
brain: scriptedBrain([
{
toolCalls: [
{
name: 'spawn_agent',
arguments: { profile: { name: 'implementer' }, task: 'implement the feature' },
},
],
},
{
toolCalls: [
{
name: 'spawn_agent',
arguments: { profile: { name: 'reviewer' }, task: 'review the implementation' },
},
],
},
// implementer settles → critique steers the live reviewer → reviewer settles → verdict
// finding reaches the driver. Four bus events: settled, finding, settled, finding.
{ toolCalls: [{ name: 'await_event', arguments: {} }] },
{ toolCalls: [{ name: 'await_event', arguments: {} }] },
{ toolCalls: [{ name: 'await_event', arguments: {} }] },
{ toolCalls: [{ name: 'await_event', arguments: {} }] },
// The re-brief: the driver folds the verdict into a second implementer spawn.
{
toolCalls: [
{
name: 'spawn_agent',
arguments: {
profile: { name: 'implementer' },
task: 'address the review verdict: add the missing tests',
},
},
],
},
{ toolCalls: [{ name: 'await_event', arguments: {} }] },
{ toolCalls: [{ name: 'await_event', arguments: {} }] },
{ content: 'done' },
]),
}
return { graph, opts }
}

export async function main(): Promise<void> {
const { graph, opts } = collaboratesReviewLoop()
const res = await runGraph(graph, opts)
printLedger('collaborates-review-loop', res)
}

if (import.meta.url === `file://${process.argv[1]}`) {
main().catch((err) => {
console.error(err)
process.exit(1)
})
}
Loading