Symphonix Documentation · Core 0.61.0

Running agents

Starting, stopping, and what a running agent is doing when nobody is asking it anything.

This is the engineering reference, rendered from the repository

It is the same file the platform's own tests are written against — LGFlow/docs/guides/running-agents.md — rendered here rather than copied, so it cannot fall behind the software it describes. It is written for somebody working on the platform and uses the internal project names: LGFlow is Symphonix Core, LGStudio is Symphonix Studio, and LGMonitor or LGStudioMonitor is the Symphonix Control Panel. Names that begin LGFlow and appear in code — LGFlowValidationError and the rest — are exported identifiers and are spelled exactly as the software spells them.

Tracing, what a turn costs, and credentials that renew themselves.

Tokens that renew themselves#

A Google access token lasts an hour, which is a poor fit for an agent that runs for weeks. A flow that names one in requires.env - GOOGLE_API_ACCESS_TOKEN, or any GOOGLE_…ACCESS_TOKEN - gets it renewed for free once the project's .env also holds the OAuth client the token came from:

GOOGLE_CLIENT_ID=
GOOGLE_CLIENT_SECRET=
GOOGLE_REFRESH_TOKEN=

buildGraph then wraps callTool: before a call it renews the token when it is within five minutes of running out, and if Google answers 401 anyway it renews at once and tries that call again - a 401 means the call did nothing, so the retry cannot do anything twice. The new token is written into the variable the tools already read, so nothing generated changes and no tool needs refresh code of its own. One renewal serves the whole process, not one per message.

Without those three settings nothing happens at all: a pasted access token works as it always did, for its hour. A generated project's .env.example offers the three lines whenever the flow names such a variable, and src/oauth.js holds the whole of it - googleTokenNames, createGoogleTokenKeeper, withFreshGoogleTokens.


What a turn costs#

A generated project's src/llm.js reports what every call spent — the model it used, tokens in and out, how many were served from the cache and how many were written to it. The journal keeps it, so the agent's own History and LGStudio's both show the tokens and the price of each call.

On Claude it also asks for that cache: the prompt is sent as one text block with a cache breakpoint on it, and Anthropic then serves the longest prefix it has seen before at a tenth of the price. Nothing is declared for it and no flow changes — what repeats is whatever repeats, which for a flow is most of what it sends: the same instructions, the same document, the same transcript, one step after another. Measured on a real agent here, one call went from $0.052 to $0.0063. A prompt too short for the provider's minimum is sent normally, with no error. OpenAI caches by itself and is charged nothing for writing one.


Tracing#

The compiler already wraps every node and router, so it can report what each one received and returned — no change to your handlers:

const graph = buildGraph(State, flow, registry, { trace: true });
-> research  [research]
     in   { question: 'How does LGFlow model loops?' }
     out  { researchResult: 'Some research result (attempt 1)', attempts: 1 }  (0.1ms)
-> evaluate  [evaluate]
     in   { question: '...', researchResult: '...', attempts: 1 }
     out  { enoughInformation: false }  (0.1ms)
  ? routeEvaluation on "evaluate" -> "retry" -> research

in is the state the handler was given; out is the update it returned — which is what LangGraph merges back into the state, not the state itself.

Pass a function instead of true to collect the events yourself — for a test assertion, a structured logger, or a UI:

const events = [];
const graph = buildGraph(State, flow, registry, { trace: (e) => events.push(e) });
EventFields
{ type: "node", phase: "enter" }id, handler, state
{ type: "node", phase: "exit" }id, handler, state, result, durationMs
{ type: "router", phase: "exit" }router, from, route, destination, state

With tracing off, handlers are registered unwrapped, so a production graph carries no overhead.

npm run example:loop has it switched on.