GNL
Docs menu
Core · Free@gnldev/durable

Dynamic agent networks

A router LLM decides at runtime which sub-agent runs; every decision freezes into the journal via CAS → resume/replay never re-calls the router, the same path is taken.

What it's for / when to use it#

With the static agent_<name> tool, you can give one agent to another as a tool — but which sub-agent gets called is fixed in the prompt flow. A network is the dynamic version of this: a router LLM (supervisor) decides on every turn, looking at the intermediate results so far, which one of the registered agents gets which task — or writes the final answer once enough has been gathered. It is the supervisor.network() pattern.

GNL's difference is determinism: every decision the router makes freezes into the <runId>:net:route:<i> key via CAS (applying the "freeze the winner" pattern from withModelFallbackto routing). When a run crashes and is resumed, the router is not called again — frozen decisions are read back, the same path is taken. Sub-agent step results also freeze to <runId>:net:step:<i>, so a completed sub-agent is skipped entirely on resume (the same synergy as the two-level durability in agent-as-tool).

Setup / definition#

Networks are defined in the createGnl configuration, under networks: . Each network takes a router model plus a list of registered agent names it can route to — when picking an agent, the router sees each agent's description(single source of truth; the same one used for agent-as-tool).

createGnl — networks definition
import { createGnl } from '@gnldev/durable';

const gnl = createGnl({
  storage,
  agents: {
    researcher: { model, description: 'Gathers facts from the web / a knowledge base' },
    writer: { model, description: 'Toplanan olgulardan taslak yazar' },
    critic: { model, description: 'Reviews the draft and asks for corrections' },
  },
  networks: {
    editorial: {
      router: candidateModels,           // spec / obje / fallback zinciri / dinamik
      agents: ['researcher', 'writer', 'critic'],
      system: 'Write a good article; loop with the critic when needed.',
      maxIterations: 6,                   // ceiling on routing turns (default 6)
    },
  },
});

Running it — runNetwork#

A network runs durably via gnl.runNetwork(name, ...) . Sub-agent call semantics are identical to agent-tool: model fallback freezes to a nested runId, run limits are inherited exactly the same way. Each sub-agent writes to its own nested journal (net:<runId>:<i>) → mid-crash resume is covered at the sub-agent level too.

runNetwork — dynamic routing
const res = await gnl.runNetwork('editorial', {
  runId: 'r1',
  task: 'Write three paragraphs explaining the GNL exactly-once guarantee.',
});
// res => { runId, text, steps: [{ i, agent, task, text }], iterations, interrupts }

If a sub-agent hits a guard and is suspended (interrupts populated), that step is not frozen — the interruption propagates upward and gets marked suspended: { i, agent, task } . When called again after approval with the same runId , the router doesn't run since the route decision is already frozen; the sub-agent resumes from its own journal, and the step freezes only once it completes.

Guarantee
maxIterations cap (default 6) is exceeded, the router is forced to finalize — an unbounded router loop is structurally impossible. If the router still can't produce a valid final after 2 attempts, the (frozen) text of the last step is returned deterministically instead of crashing.

Studio — the dynamic tree#

Studio's GET /runs/:id/network endpoint reads the route and step records from the journal and returns the network's dynamic tree (the Networks view). This is derived after the fact: getNetworkTrace extracts router decisions and executed steps from the journal — no live instrumentation needed, complete even after a crash.

getNetworkTrace — observability
import { getNetworkTrace } from '@gnldev/durable';

const tree = await getNetworkTrace(journal, 'r1');
// tree => { routes: [{ i, decision }], steps: [{ i, agent, task, text }] }

API reference#

fncreateGnl({ networks })

networks: Record<string, NetworkConfig> — each network takes a router model + the routable agent names + optional system/maxIterations.

fngnl.runNetwork

(name, { runId, task, context?, limits?, approvals? }) → NetworkResult. The router picks a sub-agent or writes the final answer on every turn; decisions are CAS-frozen.

fnrunNetwork

Registry-free core: freezes the route decision to <runId>:net:route:<i>, the step result to <runId>:net:step:<i>; interrupts propagate upward.

fngetNetworkTrace

Extracts a network run's dynamic tree (routes + steps) from the journal — the source for Studio's Networks view.

typeNetworkResult

{ runId, text, steps, iterations, stopped?, interrupts, suspended? }.

typeNetworkConfig

{ router, agents, system?, maxIterations? } — a createGnl networks entry.

Tip
Nested sub-agent runs (net:<runId>:<i> ) also show up as separate top-level runs in Studio's Runs list (the nested-run ontology) — the network tree links them hierarchically, and in the single-run inspector each one can be examined with its own journal timeline.