docs(thesis): resolve review findings and verify examples

This commit is contained in:
lda
2026-09-14 13:40:11 +07:00 Verified
parent 5495dc5656
commit 396caa63d6
8 changed files with 273 additions and 45 deletions
+154 -37
View File
@@ -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 -->
![n8n Merge configured to append two inputs, with its output preview. Author's screenshot.](assets/comparison/n8n-merge-append.png){#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 -->
![A Zapier Paths condition and its matching test record. Author's screenshot.](assets/comparison/zapier-path-condition.png){#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.