What a run shows
Reading a run: the tree, the journal, and the four questions kept apart.
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}}" }
]- Three types. A
cardhas any of a title, a line of text, label/value items and a progress bar - enough for a scoreboard, a status, a summary. Animageshows a file channel. Anactionis a button. - Three places.
"pinned"sits above the conversation and is redrawn when what it shows changes.{ "after": "<node>" }drops the view into the reply each time that node finishes.{ "with": "<ask node>" }is for actions: the button is drawn inside that question, under what it shows. - A button is an answer. Pressing an action sends its fixed
sendstext as the answer to the question, exactly as if it had been typed - so what the press means is decided by the flow's routes, testable without a page, and a terminal user can type the same words:{ "id": "hint_button", "type": "action", "place": { "with": "ask_user" }, "icon": "💡", "label": "רמז", "sends": "רמז" }. - Templates, as everywhere.
{{channel}}and paths -{{round_words.length}}counts a list - checked against the declared channels like a prompt is. A view appears once every channel it reads has a value.
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 value | drawn as |
|---|---|
| several records sharing fields | a table, one row each |
| one record with named fields | a form with nothing to fill in, labels and all |
| anything else | text, 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
replyNothing 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
replyA run nobody triggered has no such pair and reads exactly as it did before. See What starts a flow.