docs(thesis): resolve review findings and verify examples
This commit is contained in:
@@ -0,0 +1,20 @@
|
||||
# Comparison screenshots
|
||||
|
||||
Author-provided captures used in the thesis's related-systems discussion.
|
||||
These are exact pixel crops; no interface text or results were redrawn.
|
||||
The originals remain in the author's local `random shit/` folder and are
|
||||
not needed to build the thesis.
|
||||
|
||||
| Asset | Original | Crop (x, y, width, height), pixels |
|
||||
| --- | --- | --- |
|
||||
| `n8n-merge-append.png` | `a9fb75.png` | 475, 100, 1125, 630 |
|
||||
| `zapier-path-condition.png` | `filter.png` | 974, 111, 657, 762 |
|
||||
|
||||
Coordinates use the top-left corner of the 1644 × 985 originals. The n8n
|
||||
crop retains configuration and output. The Zapier crop retains the condition
|
||||
and test result while excluding the account header and unrelated graph nodes.
|
||||
|
||||
The screenshots illustrate configuration and sample results. Execution
|
||||
ordering and synchronization claims use the documentation cited in the
|
||||
manuscript. The Edit Fields capture (`fca1d3.png`) was omitted because its
|
||||
field-assignment example adds a separate topic to this comparison.
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 35 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 40 KiB |
@@ -6,13 +6,16 @@
|
||||
\usepackage[dvipsnames]{xcolor}
|
||||
\usepackage{fancyhdr}
|
||||
\usepackage{float}
|
||||
% Let screenshots share a page with their explanation before using float pages.
|
||||
\floatplacement{figure}{htbp}
|
||||
\renewcommand{\topfraction}{0.85}
|
||||
\renewcommand{\textfraction}{0.1}
|
||||
\renewcommand{\floatpagefraction}{0.75}
|
||||
\pagestyle{fancy}
|
||||
\usepackage{seqsplit}
|
||||
|
||||
% Pandoc emits inline code as \texttt{...}. This blunt wrapper keeps long
|
||||
% paths and commands from overflowing PDF table cells.
|
||||
\let\origtexttt\texttt
|
||||
\renewcommand{\texttt}[1]{{\origtexttt{\seqsplit{#1}}}}
|
||||
% Keep inline identifiers intact. Move unusually long paths to a code block
|
||||
% or shorten their prose label instead of splitting callable names mid-word.
|
||||
% Libertinus Mono permits ordinary word hyphenation, so disable that too.
|
||||
\DeclareTextFontCommand{\texttt}{\ttfamily\hyphenchar\font=-1\relax}
|
||||
|
||||
\usepackage{fvextra}
|
||||
\fvset{breaklines=true, breaknonspaceingroup=true, breakanywhere=true}
|
||||
|
||||
@@ -67,10 +67,9 @@ diagram:
|
||||
Preparing reports, transforming documents, and collecting workspace information
|
||||
often involve procedures that must be repeated with new inputs. An AI assistant
|
||||
can help perform such work. Repeating it requires preserving the operations
|
||||
and their data connections as an executable procedure. This thesis presents
|
||||
`lda.chat`, a
|
||||
programmable workflow platform for defining, checking, running, and inspecting
|
||||
reusable workspace procedures.
|
||||
and their data connections as an executable procedure. This thesis develops
|
||||
the programmable workflow subsystem of `lda.chat` for defining, checking,
|
||||
running, and inspecting reusable workspace procedures.
|
||||
|
||||
A workflow describes the operations to perform, the data they exchange, and
|
||||
the decisions that select the next step. An **artifact** is an immutable saved
|
||||
@@ -88,10 +87,10 @@ choice to separate a node's data output from its routing outcome.
|
||||
|
||||
The implementation includes a workflow runtime, Python authoring objects, a
|
||||
workflow service, and adapters for trusted Python functions and external tools.
|
||||
Evidence includes
|
||||
a deterministic case study and focused tests of validation,
|
||||
execution, and persistence. Authoring usability remains to be evaluated as
|
||||
the interaction design develops.
|
||||
The evaluation focuses on workflow construction and execution through the
|
||||
Python client. It uses a deterministic case study and identifies focused
|
||||
tests of validation, execution, and persistence. Agent-driven authoring and
|
||||
authoring usability remain to be evaluated as the interaction design develops.
|
||||
|
||||
# Introduction
|
||||
|
||||
@@ -327,6 +326,13 @@ or produce combinations. The node's configuration and worked examples make
|
||||
these operations distinguishable. Append waits for connected inputs and emits
|
||||
their items in input order [@n8n-merge-2026].
|
||||
|
||||
[@fig:n8n-merge-append] shows the Append setting and the resulting two
|
||||
output records. The fields remain in separate records; selecting Append
|
||||
does not combine them into one record.
|
||||
|
||||
<!-- markdownlint-disable-next-line MD013 -->
|
||||
{#fig:n8n-merge-append width=100%}
|
||||
|
||||
Selecting a merge mode changes which records appear in the output.
|
||||
For the report task, joining actions by owner is different from appending two
|
||||
action lists.
|
||||
@@ -356,6 +362,14 @@ distinguish complete records from records needing attention. Multiple paths
|
||||
can qualify, so exclusivity must follow from the rules rather than the
|
||||
branching appearance alone [@zapier-paths-2026].
|
||||
|
||||
[@fig:zapier-path-condition] shows a rule testing whether the selected
|
||||
`Hello` field contains `world`. The test record matches, and the interface
|
||||
reports that this path would have continued. This preview lets the author
|
||||
check a rule against a concrete value before running the workflow.
|
||||
|
||||
<!-- markdownlint-disable-next-line MD013 -->
|
||||
{#fig:zapier-path-condition width=75%}
|
||||
|
||||
The same documentation describes sequential execution of qualifying paths.
|
||||
The rules can permit several branches to execute in one run. An exclusive
|
||||
choice requires conditions that cannot qualify together.
|
||||
@@ -367,6 +381,10 @@ The interface therefore needs to communicate both the condition being tested
|
||||
and the consequence of a match. In lda.chat, an ordinary outcome selects
|
||||
one successor, whereas several Zapier Paths may qualify.
|
||||
|
||||
```{=latex}
|
||||
\begin{minipage}{\linewidth}
|
||||
```
|
||||
|
||||
## LangGraph: describing decisions in Python
|
||||
|
||||
LangGraph's Graph API lets an author define state, add node functions and
|
||||
@@ -374,6 +392,47 @@ routing, compile the graph, and invoke it. For the report task, a developer can
|
||||
represent extracted fields in state and write a routing function that selects
|
||||
what happens next [@langgraph-graph-api-2026].
|
||||
|
||||
In this small example, `clean` returns an update to the `text` field.
|
||||
The separate `choose` function reads that updated state and selects either
|
||||
the rendering step or the end of the graph:
|
||||
|
||||
<!-- langgraph-comparison -->
|
||||
```python
|
||||
from typing import TypedDict
|
||||
from langgraph.graph import START, END, StateGraph
|
||||
|
||||
class ReportState(TypedDict):
|
||||
text: str
|
||||
report: str
|
||||
|
||||
def clean(state: ReportState):
|
||||
return {"text": state["text"].strip()}
|
||||
|
||||
def choose(state: ReportState):
|
||||
return "render" if state["text"] else END
|
||||
|
||||
def render(state: ReportState):
|
||||
return {"report": "# Report\n\n" + state["text"]}
|
||||
|
||||
builder = StateGraph(ReportState)
|
||||
builder.add_node("clean", clean)
|
||||
builder.add_node("render", render)
|
||||
builder.add_edge(START, "clean")
|
||||
builder.add_conditional_edges("clean", choose, ["render", END])
|
||||
builder.add_edge("render", END)
|
||||
graph = builder.compile()
|
||||
```
|
||||
|
||||
```{=latex}
|
||||
\end{minipage}
|
||||
```
|
||||
|
||||
Calling `graph.invoke({"text": " Notes ", "report": ""})` cleans the text,
|
||||
then produces a report. Whitespace-only input ends after `clean`. The state
|
||||
update and the routing decision are expressed in separate functions here;
|
||||
LangGraph also offers `Command` to return both from a node
|
||||
[@langgraph-graph-api-2026].
|
||||
|
||||
This authoring style exposes more behavior as code. Nodes produce state
|
||||
updates, reducers determine how updates combine, and conditional routes select
|
||||
subsequent execution. The documented model supports graph loops and super-step
|
||||
@@ -417,8 +476,8 @@ mapping selects a configured provider that must satisfy the saved source
|
||||
requirements. An app connection or credential is therefore only a partial
|
||||
analogy: the source also supplies operations and their contracts.
|
||||
|
||||
The design contribution is the composition of these established lifecycle
|
||||
responsibilities with the typed graph and client interface. Each system must
|
||||
The system combines these lifecycle responsibilities with its typed graph
|
||||
and Python client. Each system must
|
||||
distinguish an edit to future work from the recorded
|
||||
definition and data of a past execution.
|
||||
|
||||
@@ -485,7 +544,7 @@ flowchart LR
|
||||
Read["Read<br/>notes"] -->|ok| Extract["Extract<br/>report"]
|
||||
Extract -->|ok| Check{Complete?}
|
||||
Check -->|ready| Render["Render<br/>report"]
|
||||
Check -->|needs_information| Ask["Request<br/>information"]
|
||||
Check -->|needs_information| Ask["Request information<br/>(interrupt)"]
|
||||
Ask -->|submitted| Render
|
||||
Render -->|ok| Finish([End])
|
||||
```
|
||||
@@ -611,7 +670,7 @@ classDiagram
|
||||
Deployment "1" <-- "0..*" Run : started from
|
||||
```
|
||||
|
||||
## Available operations and environment binding
|
||||
## Available operations and deployment binding
|
||||
|
||||
A **capability** describes functionality available from a source. The report
|
||||
example uses operation capabilities, such as extraction and rendering. A
|
||||
@@ -622,7 +681,7 @@ A **source** groups capabilities under a configured identity. The extraction
|
||||
operation might come from trusted Python code, while another operation or
|
||||
document is supplied by an external service.
|
||||
|
||||
An **environment binding**, also called a **deployment binding**, connects a
|
||||
A **deployment binding** connects a
|
||||
logical source requirement in the workflow
|
||||
to a concrete source in the environment. Validation checks whether that source
|
||||
exists and matches the saved requirements. **Source drift** means those
|
||||
@@ -767,6 +826,15 @@ Refreshing it requests a new snapshot. This makes network
|
||||
activity explicit, although applications must decide when to refresh and how
|
||||
to present progress.
|
||||
|
||||
A local/static server can also start deployment runs on a schedule. The
|
||||
optional scheduler supports one-time and recurring starts and records each
|
||||
scheduled occurrence for inspection. The Python client's `App.create_schedule()`
|
||||
registers a schedule for an existing deployment; `App.schedule()` retrieves
|
||||
its configuration, and `App.schedule_occurrences()` retrieves its history.
|
||||
The server must have scheduling enabled to start runs without a connected
|
||||
client. The Limitations chapter discusses scheduling's storage and
|
||||
concurrency constraints.
|
||||
|
||||
The draft API and CLI also support persisted editing workspaces, which retain
|
||||
unfinished definitions between requests. Python authoring can save directly
|
||||
from its editable object. Both routes produce a saved definition for deployment
|
||||
@@ -775,7 +843,8 @@ and execution.
|
||||
## Executing the graph
|
||||
|
||||
The execution core is the runtime that runs the saved graph. It holds the
|
||||
workflow input, working state, and current execution positions. To execute
|
||||
workflow input, working state, and the active node for each in-flight item
|
||||
or call. To execute
|
||||
an ordinary operation node, it resolves the node's input bindings, calls the
|
||||
selected operation, checks its result, applies output bindings, and follows
|
||||
the edge selected by the outcome. Repeating that cycle advances the run until
|
||||
@@ -818,8 +887,8 @@ flowchart TB
|
||||
|
||||
A declared outcome such as `needs_information` is a workflow decision. A
|
||||
handler exception or exhausted node-execution allowance is an execution
|
||||
failure. A workflow can therefore complete with a non-success business outcome
|
||||
without being a failed runtime execution. Where iteration supports collecting item
|
||||
failure. A workflow can complete with an outcome such as `needs_information`
|
||||
without failing the run. Where iteration supports collecting item
|
||||
errors, that policy must be explicit; errors do not automatically become
|
||||
ordinary outcome edges.
|
||||
|
||||
@@ -841,7 +910,8 @@ a time. An output binding appends each rendered report to a `reports`
|
||||
state field, which the assembly step reads.
|
||||
|
||||
[@fig:foreach-graph] shows the authored graph. The `loop` edge enters the
|
||||
item body. Its return edge, `render.ok -> each`, ends that item's work.
|
||||
item body. Its return edge (a back-edge), `render.ok -> each`, ends that
|
||||
item's work.
|
||||
The `done` edge leads to the continuation after the collection is processed.
|
||||
|
||||
<!-- markdownlint-disable-next-line MD013 -->
|
||||
@@ -862,7 +932,9 @@ controller from `Prepare documents` starts an invocation over the collection.
|
||||
|
||||
[@fig:foreach-region] follows one invocation from start to finish. Read
|
||||
downward for time. Solid arrows request work or apply a data binding;
|
||||
dashed arrows report completion. The labels identify the data and routes.
|
||||
dashed arrows report completion. Here line style distinguishes requests from
|
||||
completion; in [@fig:report-state], it distinguishes execution from data
|
||||
bindings. The labels identify the data and routes.
|
||||
|
||||
<!-- markdownlint-disable-next-line MD013 -->
|
||||
```{.mermaid #fig:foreach-region width=95% caption="Serial iteration finishes both documents before assembly."}
|
||||
@@ -888,7 +960,7 @@ The same authored render node executes twice. The runtime distinguishes
|
||||
those executions so that finishing A advances to B, while finishing B permits
|
||||
assembly. Each item returns to the foreach invocation that started it.
|
||||
|
||||
The back-edge in [@fig:foreach-graph] records the item's return. Its output
|
||||
The return edge in [@fig:foreach-graph] records the item's return. Its output
|
||||
binding appends the report before that return. A node in the body belongs to
|
||||
that loop's control region. The validator rejects using the same node both
|
||||
inside and outside the body, or returning past the immediate enclosing loop.
|
||||
@@ -915,7 +987,7 @@ The parent item then continues to its own return edge.
|
||||
sequenceDiagram
|
||||
participant Item as Parent item A
|
||||
participant Child as Child workflow
|
||||
participant Result as Item A reports writes
|
||||
participant Result as A's pending report writes
|
||||
participant Each as Foreach
|
||||
Item->>Child: Input binding: document = A.md
|
||||
activate Child
|
||||
@@ -941,7 +1013,7 @@ its child does not permit assembly while B is still running. The foreach
|
||||
waits for its required item completions before following `done`.
|
||||
|
||||
Concurrent items retain separate pending writes until their results combine.
|
||||
In [@fig:scope-boundaries], “Item A reports writes” represents these pending
|
||||
In [@fig:scope-boundaries], “A's pending report writes” represents these pending
|
||||
writes. In serial mode, the output binding updates enclosing state so the next
|
||||
item can read it.
|
||||
|
||||
@@ -1005,9 +1077,9 @@ re-inspects the exact artifact version. The resulting artifact object is
|
||||
therefore reconstructed from the stored definition. Identity checks reject
|
||||
responses identifying a different artifact or deployment.
|
||||
|
||||
The service's `inspect_run` operation retrieves a stored run by its identifier.
|
||||
The Python client's `Run.refresh()` method calls that operation, verifies the
|
||||
run and deployment identities, and returns a new run snapshot. `Run.resume()`
|
||||
The Python client's `Run.refresh()` method retrieves a stored run by its
|
||||
identifier through the service's `inspect_run` operation. It verifies the
|
||||
run and deployment identities and returns a new run snapshot. `Run.resume()`
|
||||
submits the requested response for an interrupted run and also returns a new
|
||||
snapshot. The caller retains the returned object to see the updated status
|
||||
and result.
|
||||
@@ -1059,7 +1131,8 @@ document's separate view until the outer foreach combines the documents.
|
||||
Publishing every serial section's write directly into shared workflow state
|
||||
would expose unfinished document results to the other concurrent items.
|
||||
|
||||
The runtime records where each execution belongs. A **frame** records one
|
||||
The runtime tracks the active node, the workflow invocation that owns its
|
||||
data, and the updates held separately by concurrent items. A **frame** records one
|
||||
execution position, including its current node and owning foreach when it is
|
||||
an iteration item. A **scope** contains the input, working state, and context
|
||||
of one workflow invocation. A child workflow has its own scope. A **lineage**
|
||||
@@ -1095,8 +1168,14 @@ unnamed current-item value would leave it unclear which one the node receives.
|
||||
|
||||
**Execution context** supplies runtime information to expressions and input
|
||||
bindings. Its foreach entries are keyed by the authored controller's identity.
|
||||
Inside the inner body, `context.foreach.documents.item` selects the enclosing
|
||||
document, while `context.foreach.actions.item` selects the current action.
|
||||
Inside the inner body, two paths distinguish the enclosing document from
|
||||
the current action:
|
||||
|
||||
```text
|
||||
context.foreach.documents.item # enclosing document
|
||||
context.foreach.actions.item # current action
|
||||
```
|
||||
|
||||
The runtime constructs these entries from the active frames. Repeated
|
||||
executions of the same node therefore resolve the paths to their own items.
|
||||
|
||||
@@ -1198,9 +1277,10 @@ Their Pydantic models describe the input and output
|
||||
contracts. The author consumes those operations from the service inventory;
|
||||
the client does not import their implementations to execute them locally.
|
||||
|
||||
The following blocks form one asynchronous Python session. The retained test
|
||||
executes these blocks against the real service in process, replacing only
|
||||
the HTTP connection assignment. It does not verify remote server startup.
|
||||
The Python blocks form one asynchronous session. The retained test runs that
|
||||
session against the real service in process. It substitutes an in-process
|
||||
connection for the HTTP connection assignment; remote server startup is not
|
||||
tested.
|
||||
For HTTP use, the blocks assume a
|
||||
server using `examples/report_workflow/wf.config.json`, reachable at its
|
||||
configured address, and a fresh artifact name or unused version. The fixture
|
||||
@@ -1444,9 +1524,9 @@ sequenceDiagram
|
||||
Client->>API: Bind deployment to version
|
||||
API-->>Client: Deployment and readiness
|
||||
Client->>API: Run with input
|
||||
API->>Provider: Invoke graph steps
|
||||
Provider-->>API: Outputs and outcomes
|
||||
API-->>Client: Stopped run snapshot
|
||||
API->>Provider: Invoke bound operation
|
||||
Provider-->>API: Output and outcome
|
||||
API-->>Client: Run snapshot
|
||||
Client->>API: Inspect run and bounded trace
|
||||
API-->>Client: Stored execution evidence
|
||||
Client-->>Author: Result or diagnostic
|
||||
@@ -1506,6 +1586,42 @@ recorded risks and followups, and a Markdown report headed
|
||||
example's data reached the intended outputs. The extraction contract requires
|
||||
the sectioned notes format described at the start of this chapter.
|
||||
|
||||
## Registering a scheduled run
|
||||
|
||||
The same deployment can be invoked later by the server's scheduler. With
|
||||
scheduling enabled on the local/static server, the client can register a
|
||||
one-time start and inspect its stored configuration and occurrence history:
|
||||
|
||||
```python
|
||||
from datetime import UTC, datetime, timedelta
|
||||
|
||||
schedule = await app.create_schedule(
|
||||
schedule_id="report-once",
|
||||
deployment_id=deployment.deployment_id,
|
||||
trigger={
|
||||
"kind": "oneshot",
|
||||
"at": (datetime.now(UTC) + timedelta(hours=1)).isoformat(),
|
||||
},
|
||||
input_bindings=[{
|
||||
"target": "text",
|
||||
"expression": {"kind": "literal", "value": notes},
|
||||
}],
|
||||
max_steps=100,
|
||||
)
|
||||
schedule = await app.schedule(schedule.schedule_id)
|
||||
history = await app.schedule_occurrences(schedule.schedule_id, limit=10)
|
||||
```
|
||||
|
||||
The input binding stores the current contents of `notes`. The scheduled run
|
||||
receives that text even if the original file changes. A recurring workflow
|
||||
that needs fresh notes would need an operation that retrieves them when it
|
||||
runs. Occurrence history records what happened to scheduled starts, including
|
||||
admission and completion. Immediately after registration, it can be empty.
|
||||
|
||||
The session check verifies registration and inspection without waiting for
|
||||
the scheduled time. Scheduler execution is exercised separately in
|
||||
[`test_scheduled_deployment_example.py`](../../tests/examples/test_scheduled_deployment_example.py).
|
||||
|
||||
## What this case demonstrates
|
||||
|
||||
The executable check in
|
||||
@@ -1520,7 +1636,7 @@ discovery and invocation, and the artifact/deployment/run lifecycle using
|
||||
the raw-plan fixture. Those tests are evidence for the report operations
|
||||
and lifecycle, complementing the Python session check.
|
||||
|
||||
The Python presentation makes the current authoring experience concrete:
|
||||
The Python session shows the current authoring steps:
|
||||
inspect operations, declare contracts, connect data and outcomes, save,
|
||||
select bindings, and inspect an execution. The CLI and draft surface offer
|
||||
alternative interfaces to related lifecycle operations.
|
||||
@@ -1540,8 +1656,9 @@ are needed to evaluate the interaction beyond this scripted session.
|
||||
|
||||
Evaluation distinguishes three questions: whether the runtime follows its
|
||||
contract, whether the public lifecycle composes correctly, and whether an
|
||||
author can use that lifecycle effectively. The current evidence addresses
|
||||
the first two through controlled tests and an adapted in-process walkthrough.
|
||||
author can use that lifecycle effectively. The reported in-process walkthrough
|
||||
exercises the public lifecycle. Targeted tests for runtime contracts are
|
||||
mapped in the Evidence Index.
|
||||
The authoring assessment identifies the operations available through the
|
||||
interface. Usability remains a separate evaluation question.
|
||||
|
||||
@@ -1845,7 +1962,7 @@ different from promising that an earlier action can be undone.
|
||||
|
||||
This report examined how a useful procedure can become a reusable workflow
|
||||
that an author can define and an operator can inspect. The implemented
|
||||
system separates the saved definition, its environment bindings, and each
|
||||
system separates the saved definition, its deployment bindings, and each
|
||||
execution into artifacts, deployments, and runs. Those distinctions give
|
||||
workflow use a record beyond the lifetime of an editing session or a single
|
||||
script invocation.
|
||||
|
||||
Reference in New Issue
Block a user