feat: UI + model revamp — Logic everywhere, recursive composition, single logic view #38
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
feat: UI + model revamp — "Logic" everywhere, recursive composition, single logic view
TL;DR
Reframe the system around logics instead of workflows. A logic is a unit of typed I/O backed by Python code. Every
@logic-decorated function inside a logic's source is itself a logic — composable all the way down. Sub-logic navigation breadcrumbs into the child's view; back out to the parent. Stop at primitive Python (imported clients, raw lines, built-ins).The UI collapses to two views only: a dashboard listing logics, and the logic view itself. Within the logic view:
Everything else — separate
/examples, separate/playslist, the dedicated/plays/{sid}detail page — goes away.1. The new mental model
A Logic has:
name(identifier, unique-ish)descriptioninputs: [FlowField]outputs: [FlowField]versions: [LogicVersion]— each carries its ownpython_sourcecurrent_version_sidA LogicVersion's
python_sourceis the Python code. Every@logic-decorated function in that source is a sub-logic. Calls to other top-level logics vialogic.invoke("name", ...)resolve to other Logic records. Either way, when the runtime executes, every@logiccall opens a span — same as today's@flow. The flow view is the visualisation of those spans (live during a play, or pinned to the latest play when idle).Stopping rule: the recursion stops at non-
@logiccode:HeroAibrokerClient().chat(...))re.sub(...),json.loads(...))So "is this a sub-logic?" = "is this a
@logic-decorated function defined inside the parent's source, or alogic.invoke("name", ...)call to a named Logic record?"2. Layout
2.1 The logic view (single page, fixed regions)
Regions are persistent. The play bar is always there. The flow column always renders whichever is the current overlay (latest play when first loaded; whichever the user picked from the Plays list inside the play bar; live one when ▶ Run is hit).
2.2 Dashboard
A single page at
/. Lists every Logic by name + description + last run status + last benchmark success rate. Click a logic → open/logics/{sid}. That's it.2.3 Routes that go away
/workflows(list) — superseded by dashboard/workflows/{sid}— renamed to/logics/{sid}; old route 302s/workflows/new— replaced by a "+ New logic" button on the dashboard/examples— examples are inline on each logic's play bar; no global page/plays(list) — plays are inline on each logic's play bar/plays/{sid}— was the dedicated detail page from #32; finishes its removal (already a redirect today)3. Conceptual rename (data + code)
Workflowand friends becomeLogic. Done as a hard rename in code with serde aliases on storage so existing OTOML records keep loading.WorkflowrootobjectLogic#[serde(alias = "Workflow")]on the type tag if/when OTOML serializes it. Field names likeworkflow_sid→logic_sidget#[serde(alias = "workflow_sid")]aliases.WorkflowVersionLogicVersionWorkflow.current_version_sidPlay.workflow_sid/Play.workflow_version_sidPlay.logic_sid/Play.logic_version_sid#[serde(alias = "workflow_sid")].Example.workflow_sid/Example.workflow_version_sidExample.logic_sid/Example.logic_version_sidBenchmark.workflow_sid/Benchmark.workflow_version_sidBenchmark.logic_sid/Benchmark.logic_version_sidLogicService(service name)LogicService.workflow_*RPC methodsLogicService.logic_*@flow(...)decorator@logic(...)flowstays exported fromhero_tracingas an alias oflogicfor one release.flow.invoke(name, ...)logic.invoke(name, ...)flow.pause(...)/ask_user.*logic.pause(...)/ask_user.*hero_tracingmodule nameThe
@flow→@logicrename is purely an alias — both names refer to the same decorator. Stored python_source that saysfrom hero_tracing import flowkeeps working; new sources sayfrom hero_tracing import logic.4. The flow view (middle column)
4.1 Idle (no overlay)
Parse the current version's
python_sourcefor@logic-decorated functions andlogic.invoke(...)calls. Render a static graph of declared sub-logics in source-order:Each node is clickable. Click a sub-logic node → breadcrumb-navigate into that sub-logic's view. The breadcrumb lives at the top of the left sidebar:
Click "service_agent" in the breadcrumb → back out to the parent. Stop at primitive calls (imported clients, stdlib): these render as leaf nodes with a "primitive" badge and are not clickable.
4.2 Active (a play is selected)
Render the actual span tree of the play. Same node shapes, but now they carry status + duration + the recorded inputs/outputs. Replayed spans dashed; failed spans red; in-progress pulse. Clicking a span node = same drill-in behaviour as idle: breadcrumb into that sub-logic, except now the sub-logic's flow view shows ITS spans inside the parent play (filtered to spans whose path descends from the clicked node). Breadcrumb back out, parent re-renders.
4.3 Code view
Toggle in the middle column header:
[Graph] [Code] [Split]. Code is the Monaco editor bound toLogicVersion.python_source. Clicking a graph node highlights the corresponding source lines.4.4 Future direction (out of scope, called out)
The flow view eventually becomes a visual code editor:
logic.invoke("...")blocks from a palette of saved logics on the right.asyncio.gatheras parallel forks.Not in this issue's scope — but the data model (
@logic+ named sub-logics + typed I/O) is designed so this is a future-compatible direction.5. The play bar (bottom, three columns)
Always visible. Heights persist via
localStorage. The three columns:5.1 Left column — Inputs + Examples + Plays history
Logic.inputs[i].field_type.Examplerecords for this logic. Click one → populate the input fields. "Save as example" button writes the current values back as a new Example.input_data. Right sidebar switches to that play's stats.logic.play_start, the new play becomes the overlay.5.2 Middle column — Live trace + Pause forms
awaiting_resume, the pause form is rendered at the top of the middle column as a banner (always visible until answered, regardless of how the user has scrolled the log feed below it).ResumeRequest.ui.kind: text / number / choice / multi_choice / confirm. Submit postsplay_resume.5.3 Right column — Output
output_dataas it accumulates.Logic.outputsis declared, render one labeled card per output field; else render raw JSON.5.4 Pause UX nit
When a play pauses, the play bar visually emphasizes the pause: middle column shifts the pause form to the top + adds an accent border. The user shouldn't have to find the pause form — the play bar makes it the most prominent thing.
6. The right stats sidebar
Two modes, switched by whether a play is currently selected:
6.1 No play selected → version benchmark stats
Shows the latest
BenchmarkforLogic.current_version_sid:6.2 Play selected → play stats
Shows that play's:
Compact. No tabs. No interaction beyond the cancel button — drill-in lives elsewhere (the flow view middle column, the play bar columns).
7. Worked example: service_agent
This is what the new UI looks like for the existing
service_agentflow. No code changes toservice_agent.pyitself — just renames + the UI rendering.7.1 Dashboard → click
service_agentUser lands on
/logics/{service_agent_sid}.7.2 Initial state (no play overlay)
Left sidebar:
Middle (flow view, idle, parsed from source):
Each sub-logic is a clickable node.
for attempt in range(3)renders as a loop container. Themodel_callinsideservice_code_genonly shows when that node is expanded.Right sidebar (benchmark stats for v3):
Bottom play bar (idle):
7.3 User clicks "Calendar event" example, hits ▶ Run
Inputs auto-fill:
prompt: "Create a calendar event titled X tomorrow at 10am",model: "". Run →logic.play_start→ new play02j7becomes the overlay.Right sidebar switches to play stats (status: running, started 0:00).
Middle column (flow view, live):
Play bar middle column — live log feed:
7.4 A step pauses with
ask_user.choice(...)Suppose
select_servicesdecides the chosen service's rootobjects don't clearly match the prompt and calls:The play exits with
awaiting_resume. Right sidebar shows play statusawaiting_resume. Play bar middle column shifts the pause form to a banner at the top:User picks "Event", submits →
play_resumeposts the answer → server respawns the subprocess with the answer cached → flow continues from where it paused withfetch_catalog,select_services,compile_stubsreplayed (dashed in the flow tree) andservice_code_genexecuting fresh.7.5 User clicks
service_code_gennode in the flow view (mid-play)Breadcrumb in left sidebar updates:
The whole logic view rerenders for
service_code_gen:service_code_gen's title, description, inputs (prompt, services), outputs (script), versions.service_code_gen's flow view, scoped to that part of the current play's span tree — just showsmodel_callinsideattempt 1.service_code_gen's contribution to this play (its tokens, its duration).service_code_gen's declared inputs, prefilled with the values the parent passed (prompt=...,services=[...]). Examples list isservice_code_gen's saved examples. Plays list shows past plays ofservice_code_genstandalone (when it was invoked as the root logic).service_code_gen's spans + descendants.If the user wants to fork off a standalone play of
service_code_genfrom here (with the prefilled inputs), they hit ▶ Run — that starts a new top-level play ofservice_code_genindependent of the parent.Click "service_agent" in the breadcrumb → zoom back out. Same play, parent view.
7.6 Authoring mid-play
If the user wants to fix something in
service_code_gen's source, they toggle Code view in the middle column header. Monaco loadsservice_code_gen's python_source. Edit. Save → creates a new LogicVersion. The current play is unaffected (it's pinned to the version it started on), but next runs use the new version. Step-memoization cache invalidates globally for that logic (the version_sid is in the step_key).8. What replaces what
/workflows(workflow list)/(logic list)/workflows/{sid}/edit(editor)/logics/{sid}(logic view)/plays(plays list page)/plays/{sid}(dedicated detail page)/examples(examples list page)9. Implementation phases
Schema + code renames with aliases. Hard-rename
Workflow*→Logic*and@flow→@logic. Add#[serde(alias = ...)]on every field + every old RPC method name. SDK exports bothflowandlogic. Old data keeps loading, old python_source keeps importing. (Closes 70% of the work; nothing visible to the user yet.)Dashboard restructure.
/becomes the logic list. Remove/workflows,/examples,/plays. 301 the old URLs to/or/logics/{sid}as appropriate.Logic view layout rebuild. Implement the three-region body (left info / middle flow-or-code / right stats) + the bottom play bar. Move inputs/examples/plays into the play bar. Move benchmark/play stats into the right sidebar. Remove the top toolbar from the editor.
Flow-view static parse. When idle, render the sub-logic graph from a parse of
python_source(find@logic-decorateddefs andlogic.invoke("...")calls). Click → breadcrumb-navigate.Breadcrumb navigation + sub-logic view. Clicking a sub-logic node loads that sub-logic's view. When a play is overlaid, the sub-logic's flow view filters the parent play's spans to descendants of the clicked node.
Pause-form prominence. Move pause forms to a banner at the top of the play bar's middle column when
awaiting_resume.Stats sidebar. Conditional rendering — benchmark stats when no play overlay, play stats when one is selected.
Cleanup. Delete
play_detail.html,examples.html,plays.html,workflows.html. Drop the old routes frommain.rs.Out of scope (called out for the future): visual flow editor with drag/drop, conditional/loop/parallel visualisation, data-flow lines, two-way graph↔code binding.
10. Acceptance
/shows a logic list with name + description + last-run status. Click → logic view./workflows/*,/examples,/plays,/plays/{sid}all 301 to the new layout or are removed.Workflowrecords read fine via serde aliases).from hero_tracing import flowkeeps working (the SDK exportsflowas an alias oflogic).Superseded by #39 — the discussion in the chat shrunk the design further (no spans, no instrument(), no inline-vs-named distinction, no Benchmark rootobject; every function is a Logic, every invocation is a Play, plays form a tree).