Symphonix Documentation · Core 0.61.0

What a run shows

Reading a run: the tree, the journal, and the four questions kept apart.

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/what-a-run-shows.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.

Views beside the conversation, and how the chat page itself looks.

How an agent's chat looks — v0.12#

A flow's page section sets how its own chat page reads, without touching the page every other agent uses:

"page": {
  "question": { "size": "large", "bold": true },
  "fontScale": 1.2, "font": "rounded", "accent": "#2f6fd0",
  "direction": "rtl", "welcome": "ברוכים הבאים!", "hide": ["history"],
  "css": ".bubble { border-radius: 22px; }"
}

The settings are checked like the rest of a flow - an unknown one, or a colour written blue, is refused rather than half applied. css covers what they do not, and wins over them. The server turns the page into one style sheet (src/page.js) and sends it with the chat page; the page's security policy still holds, so a style sheet can change how the chat looks and nothing else - no outside fonts, pictures or @import. See the page topic of the reference.


Views — v0.11#

Every agent gets the same chat page, and until now the only way to put data in front of the person was words: an answer at the end, or a question. A flow can now say how its data is shown, beside its state and its nodes:

"views": [
  { "id": "scoreboard", "type": "card", "place": "pinned", "title": "Your score",
    "items": [{ "label": "Points", "value": "{{score}}" }],
    "progress": { "value": "{{current_index}}", "max": "{{round_words.length}}" } },
  { "id": "word", "type": "image", "place": { "after": "get_image" }, "file": "{{current_image}}" }
]

A view only displays. It never writes state or decides anything, so it needs no generated code, adding one cannot change what the agent does, and a run with no page - a terminal, a test - ignores it. Deciding stays in the flow.

Rendered where the state is. A node's writes go through reducers before they are the state, so the page could not work a view out from the updates it is shown. run.js --serve fills views in from the real state instead - at each node's start, which is the first moment the previous node's writes are the state, and where the run stops - and sends the page { "type": "view", ... } events: finished cards, never the state behind them. A pinned view is sent only when it changes; the page keeps the last one.

The page is the same one for every agent: it knows how to draw a card, an image and a button, and each flow says which to draw, where, and from what. Its colours, fonts and layout are not the flow's to change.

Everywhere a view does not say — v0.17#

A view is for data you have decided to put somewhere particular. Everywhere else a value reaches a person — what an ask node shows, what an approval card shows — its shape decides how it is drawn, and no flow has to ask:

the valuedrawn as
several records sharing fieldsa table, one row each
one record with named fieldsa form with nothing to fill in, labels and all
anything elsetext, as before

customerId is labelled "Customer id", an empty field reads as — rather than a gap, and a record inside a record is drawn the same way. So a prompt no longer has to beg for a Markdown table to get one, and a flow no longer needs a view per channel to avoid printing JSON at somebody.

The answer is drawn this way too, and a string channel is no obstacle: an answer that is wholly one JSON record, or one JSON list of records, is read back as what it is. That is what lets a flow choose, per turn, between showing and describing:

When the person asked to see records and the calls returned those records, answer with ONE JSON value and nothing else. Answer in plain words instead when the turn did something rather than found something.

The first gets a form or a table; the second stays a sentence. Prose with a brace in it stays prose, and a bare number or a list of plain values is never mistaken for a record.


A run made of several agents (v0.19)#

A flat list is enough for one flow and a pile for an orchestrator calling three agents. So each delegation is traced as a pair:

{ type: "agent", phase: "enter", id, agent, parentAgent, invocationId, runId, parentRunId, input, contract }
{ type: "agent", phase: "exit",  id, agent, invocationId, runId, ok, durationMs, output | error }

and every event inside carries the child's runId and agent, with its step named <node>/<agent>/<their node>. That is enough to rebuild the tree:

import { agentTree, describeAgentTree } from "lgflow";
console.log(describeAgentTree(agentTree(events)));
list_desks
decide
delegate -> sql (ok, 11ms)
  write_query
  run_query
  say
reply

Nothing about a single-flow trace changed: an event with no runId belongs to the top flow, which is what every trace looked like before. See Many agents.

Why the run exists (v0.20)#

A run something started traces one more pair around the rest, on the same tracer:

{ type: "trigger", phase: "enter", triggerId, triggerType, runId, triggeredAt, input, eventName? | method?/path? }
{ type: "trigger", phase: "exit",  triggerId, runId, ok, durationMs, outputs | error }

triggerTree(events) returns the v0.19 agent tree with that on top of it, so one trace holds trigger, flow, agent and child flow:

Schedule -> daily-report (ok, 95ms)
  sort
  ask_sales -> sales (ok, 11ms)
    look
  reply

A run nobody triggered has no such pair and reads exactly as it did before. See What starts a flow.