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[dvipsnames]{xcolor}
|
||||||
\usepackage{fancyhdr}
|
\usepackage{fancyhdr}
|
||||||
\usepackage{float}
|
\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}
|
\pagestyle{fancy}
|
||||||
\usepackage{seqsplit}
|
% Keep inline identifiers intact. Move unusually long paths to a code block
|
||||||
|
% or shorten their prose label instead of splitting callable names mid-word.
|
||||||
% Pandoc emits inline code as \texttt{...}. This blunt wrapper keeps long
|
% Libertinus Mono permits ordinary word hyphenation, so disable that too.
|
||||||
% paths and commands from overflowing PDF table cells.
|
\DeclareTextFontCommand{\texttt}{\ttfamily\hyphenchar\font=-1\relax}
|
||||||
\let\origtexttt\texttt
|
|
||||||
\renewcommand{\texttt}[1]{{\origtexttt{\seqsplit{#1}}}}
|
|
||||||
|
|
||||||
\usepackage{fvextra}
|
\usepackage{fvextra}
|
||||||
\fvset{breaklines=true, breaknonspaceingroup=true, breakanywhere=true}
|
\fvset{breaklines=true, breaknonspaceingroup=true, breakanywhere=true}
|
||||||
|
|||||||
@@ -67,10 +67,9 @@ diagram:
|
|||||||
Preparing reports, transforming documents, and collecting workspace information
|
Preparing reports, transforming documents, and collecting workspace information
|
||||||
often involve procedures that must be repeated with new inputs. An AI assistant
|
often involve procedures that must be repeated with new inputs. An AI assistant
|
||||||
can help perform such work. Repeating it requires preserving the operations
|
can help perform such work. Repeating it requires preserving the operations
|
||||||
and their data connections as an executable procedure. This thesis presents
|
and their data connections as an executable procedure. This thesis develops
|
||||||
`lda.chat`, a
|
the programmable workflow subsystem of `lda.chat` for defining, checking,
|
||||||
programmable workflow platform for defining, checking, running, and inspecting
|
running, and inspecting reusable workspace procedures.
|
||||||
reusable workspace procedures.
|
|
||||||
|
|
||||||
A workflow describes the operations to perform, the data they exchange, and
|
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
|
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
|
The implementation includes a workflow runtime, Python authoring objects, a
|
||||||
workflow service, and adapters for trusted Python functions and external tools.
|
workflow service, and adapters for trusted Python functions and external tools.
|
||||||
Evidence includes
|
The evaluation focuses on workflow construction and execution through the
|
||||||
a deterministic case study and focused tests of validation,
|
Python client. It uses a deterministic case study and identifies focused
|
||||||
execution, and persistence. Authoring usability remains to be evaluated as
|
tests of validation, execution, and persistence. Agent-driven authoring and
|
||||||
the interaction design develops.
|
authoring usability remain to be evaluated as the interaction design develops.
|
||||||
|
|
||||||
# Introduction
|
# 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
|
these operations distinguishable. Append waits for connected inputs and emits
|
||||||
their items in input order [@n8n-merge-2026].
|
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.
|
Selecting a merge mode changes which records appear in the output.
|
||||||
For the report task, joining actions by owner is different from appending two
|
For the report task, joining actions by owner is different from appending two
|
||||||
action lists.
|
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
|
can qualify, so exclusivity must follow from the rules rather than the
|
||||||
branching appearance alone [@zapier-paths-2026].
|
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 same documentation describes sequential execution of qualifying paths.
|
||||||
The rules can permit several branches to execute in one run. An exclusive
|
The rules can permit several branches to execute in one run. An exclusive
|
||||||
choice requires conditions that cannot qualify together.
|
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
|
and the consequence of a match. In lda.chat, an ordinary outcome selects
|
||||||
one successor, whereas several Zapier Paths may qualify.
|
one successor, whereas several Zapier Paths may qualify.
|
||||||
|
|
||||||
|
```{=latex}
|
||||||
|
\begin{minipage}{\linewidth}
|
||||||
|
```
|
||||||
|
|
||||||
## LangGraph: describing decisions in Python
|
## LangGraph: describing decisions in Python
|
||||||
|
|
||||||
LangGraph's Graph API lets an author define state, add node functions and
|
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
|
represent extracted fields in state and write a routing function that selects
|
||||||
what happens next [@langgraph-graph-api-2026].
|
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
|
This authoring style exposes more behavior as code. Nodes produce state
|
||||||
updates, reducers determine how updates combine, and conditional routes select
|
updates, reducers determine how updates combine, and conditional routes select
|
||||||
subsequent execution. The documented model supports graph loops and super-step
|
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
|
requirements. An app connection or credential is therefore only a partial
|
||||||
analogy: the source also supplies operations and their contracts.
|
analogy: the source also supplies operations and their contracts.
|
||||||
|
|
||||||
The design contribution is the composition of these established lifecycle
|
The system combines these lifecycle responsibilities with its typed graph
|
||||||
responsibilities with the typed graph and client interface. Each system must
|
and Python client. Each system must
|
||||||
distinguish an edit to future work from the recorded
|
distinguish an edit to future work from the recorded
|
||||||
definition and data of a past execution.
|
definition and data of a past execution.
|
||||||
|
|
||||||
@@ -485,7 +544,7 @@ flowchart LR
|
|||||||
Read["Read<br/>notes"] -->|ok| Extract["Extract<br/>report"]
|
Read["Read<br/>notes"] -->|ok| Extract["Extract<br/>report"]
|
||||||
Extract -->|ok| Check{Complete?}
|
Extract -->|ok| Check{Complete?}
|
||||||
Check -->|ready| Render["Render<br/>report"]
|
Check -->|ready| Render["Render<br/>report"]
|
||||||
Check -->|needs_information| Ask["Request<br/>information"]
|
Check -->|needs_information| Ask["Request information<br/>(interrupt)"]
|
||||||
Ask -->|submitted| Render
|
Ask -->|submitted| Render
|
||||||
Render -->|ok| Finish([End])
|
Render -->|ok| Finish([End])
|
||||||
```
|
```
|
||||||
@@ -611,7 +670,7 @@ classDiagram
|
|||||||
Deployment "1" <-- "0..*" Run : started from
|
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
|
A **capability** describes functionality available from a source. The report
|
||||||
example uses operation capabilities, such as extraction and rendering. A
|
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
|
operation might come from trusted Python code, while another operation or
|
||||||
document is supplied by an external service.
|
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
|
logical source requirement in the workflow
|
||||||
to a concrete source in the environment. Validation checks whether that source
|
to a concrete source in the environment. Validation checks whether that source
|
||||||
exists and matches the saved requirements. **Source drift** means those
|
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
|
activity explicit, although applications must decide when to refresh and how
|
||||||
to present progress.
|
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
|
The draft API and CLI also support persisted editing workspaces, which retain
|
||||||
unfinished definitions between requests. Python authoring can save directly
|
unfinished definitions between requests. Python authoring can save directly
|
||||||
from its editable object. Both routes produce a saved definition for deployment
|
from its editable object. Both routes produce a saved definition for deployment
|
||||||
@@ -775,7 +843,8 @@ and execution.
|
|||||||
## Executing the graph
|
## Executing the graph
|
||||||
|
|
||||||
The execution core is the runtime that runs the saved graph. It holds the
|
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
|
an ordinary operation node, it resolves the node's input bindings, calls the
|
||||||
selected operation, checks its result, applies output bindings, and follows
|
selected operation, checks its result, applies output bindings, and follows
|
||||||
the edge selected by the outcome. Repeating that cycle advances the run until
|
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
|
A declared outcome such as `needs_information` is a workflow decision. A
|
||||||
handler exception or exhausted node-execution allowance is an execution
|
handler exception or exhausted node-execution allowance is an execution
|
||||||
failure. A workflow can therefore complete with a non-success business outcome
|
failure. A workflow can complete with an outcome such as `needs_information`
|
||||||
without being a failed runtime execution. Where iteration supports collecting item
|
without failing the run. Where iteration supports collecting item
|
||||||
errors, that policy must be explicit; errors do not automatically become
|
errors, that policy must be explicit; errors do not automatically become
|
||||||
ordinary outcome edges.
|
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.
|
state field, which the assembly step reads.
|
||||||
|
|
||||||
[@fig:foreach-graph] shows the authored graph. The `loop` edge enters the
|
[@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.
|
The `done` edge leads to the continuation after the collection is processed.
|
||||||
|
|
||||||
<!-- markdownlint-disable-next-line MD013 -->
|
<!-- 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
|
[@fig:foreach-region] follows one invocation from start to finish. Read
|
||||||
downward for time. Solid arrows request work or apply a data binding;
|
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 -->
|
<!-- markdownlint-disable-next-line MD013 -->
|
||||||
```{.mermaid #fig:foreach-region width=95% caption="Serial iteration finishes both documents before assembly."}
|
```{.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
|
those executions so that finishing A advances to B, while finishing B permits
|
||||||
assembly. Each item returns to the foreach invocation that started it.
|
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
|
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
|
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.
|
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
|
sequenceDiagram
|
||||||
participant Item as Parent item A
|
participant Item as Parent item A
|
||||||
participant Child as Child workflow
|
participant Child as Child workflow
|
||||||
participant Result as Item A reports writes
|
participant Result as A's pending report writes
|
||||||
participant Each as Foreach
|
participant Each as Foreach
|
||||||
Item->>Child: Input binding: document = A.md
|
Item->>Child: Input binding: document = A.md
|
||||||
activate Child
|
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`.
|
waits for its required item completions before following `done`.
|
||||||
|
|
||||||
Concurrent items retain separate pending writes until their results combine.
|
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
|
writes. In serial mode, the output binding updates enclosing state so the next
|
||||||
item can read it.
|
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
|
therefore reconstructed from the stored definition. Identity checks reject
|
||||||
responses identifying a different artifact or deployment.
|
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 retrieves a stored run by its
|
||||||
The Python client's `Run.refresh()` method calls that operation, verifies the
|
identifier through the service's `inspect_run` operation. It verifies the
|
||||||
run and deployment identities, and returns a new run snapshot. `Run.resume()`
|
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
|
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
|
snapshot. The caller retains the returned object to see the updated status
|
||||||
and result.
|
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
|
Publishing every serial section's write directly into shared workflow state
|
||||||
would expose unfinished document results to the other concurrent items.
|
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
|
execution position, including its current node and owning foreach when it is
|
||||||
an iteration item. A **scope** contains the input, working state, and context
|
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**
|
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
|
**Execution context** supplies runtime information to expressions and input
|
||||||
bindings. Its foreach entries are keyed by the authored controller's identity.
|
bindings. Its foreach entries are keyed by the authored controller's identity.
|
||||||
Inside the inner body, `context.foreach.documents.item` selects the enclosing
|
Inside the inner body, two paths distinguish the enclosing document from
|
||||||
document, while `context.foreach.actions.item` selects the current action.
|
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
|
The runtime constructs these entries from the active frames. Repeated
|
||||||
executions of the same node therefore resolve the paths to their own items.
|
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;
|
contracts. The author consumes those operations from the service inventory;
|
||||||
the client does not import their implementations to execute them locally.
|
the client does not import their implementations to execute them locally.
|
||||||
|
|
||||||
The following blocks form one asynchronous Python session. The retained test
|
The Python blocks form one asynchronous session. The retained test runs that
|
||||||
executes these blocks against the real service in process, replacing only
|
session against the real service in process. It substitutes an in-process
|
||||||
the HTTP connection assignment. It does not verify remote server startup.
|
connection for the HTTP connection assignment; remote server startup is not
|
||||||
|
tested.
|
||||||
For HTTP use, the blocks assume a
|
For HTTP use, the blocks assume a
|
||||||
server using `examples/report_workflow/wf.config.json`, reachable at its
|
server using `examples/report_workflow/wf.config.json`, reachable at its
|
||||||
configured address, and a fresh artifact name or unused version. The fixture
|
configured address, and a fresh artifact name or unused version. The fixture
|
||||||
@@ -1444,9 +1524,9 @@ sequenceDiagram
|
|||||||
Client->>API: Bind deployment to version
|
Client->>API: Bind deployment to version
|
||||||
API-->>Client: Deployment and readiness
|
API-->>Client: Deployment and readiness
|
||||||
Client->>API: Run with input
|
Client->>API: Run with input
|
||||||
API->>Provider: Invoke graph steps
|
API->>Provider: Invoke bound operation
|
||||||
Provider-->>API: Outputs and outcomes
|
Provider-->>API: Output and outcome
|
||||||
API-->>Client: Stopped run snapshot
|
API-->>Client: Run snapshot
|
||||||
Client->>API: Inspect run and bounded trace
|
Client->>API: Inspect run and bounded trace
|
||||||
API-->>Client: Stored execution evidence
|
API-->>Client: Stored execution evidence
|
||||||
Client-->>Author: Result or diagnostic
|
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
|
example's data reached the intended outputs. The extraction contract requires
|
||||||
the sectioned notes format described at the start of this chapter.
|
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
|
## What this case demonstrates
|
||||||
|
|
||||||
The executable check in
|
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
|
the raw-plan fixture. Those tests are evidence for the report operations
|
||||||
and lifecycle, complementing the Python session check.
|
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,
|
inspect operations, declare contracts, connect data and outcomes, save,
|
||||||
select bindings, and inspect an execution. The CLI and draft surface offer
|
select bindings, and inspect an execution. The CLI and draft surface offer
|
||||||
alternative interfaces to related lifecycle operations.
|
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
|
Evaluation distinguishes three questions: whether the runtime follows its
|
||||||
contract, whether the public lifecycle composes correctly, and whether an
|
contract, whether the public lifecycle composes correctly, and whether an
|
||||||
author can use that lifecycle effectively. The current evidence addresses
|
author can use that lifecycle effectively. The reported in-process walkthrough
|
||||||
the first two through controlled tests and an adapted 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
|
The authoring assessment identifies the operations available through the
|
||||||
interface. Usability remains a separate evaluation question.
|
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
|
This report examined how a useful procedure can become a reusable workflow
|
||||||
that an author can define and an operator can inspect. The implemented
|
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
|
execution into artifacts, deployments, and runs. Those distinctions give
|
||||||
workflow use a record beyond the lifetime of an editing session or a single
|
workflow use a record beyond the lifetime of an editing session or a single
|
||||||
script invocation.
|
script invocation.
|
||||||
|
|||||||
@@ -58,3 +58,46 @@ def test_abbreviations_compile_without_a_dummy_float(tmp_path: Path) -> None:
|
|||||||
assert "API" in rendered
|
assert "API" in rendered
|
||||||
assert "Application Programming Interface" in rendered
|
assert "Application Programming Interface" in rendered
|
||||||
assert "None 1" not in rendered
|
assert "None 1" not in rendered
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.skipif(
|
||||||
|
any(shutil.which(tool) is None for tool in ("pandoc", "xelatex", "pdftotext")),
|
||||||
|
reason="Pandoc, XeLaTeX, and pdftotext are needed for the inline-code build",
|
||||||
|
)
|
||||||
|
def test_inline_identifier_moves_to_next_line_intact(tmp_path: Path) -> None:
|
||||||
|
"""A prose line ending must not split a callable's name into fragments."""
|
||||||
|
output = tmp_path / "inline.pdf"
|
||||||
|
identifier = "local.report.render_markdown_report"
|
||||||
|
result = subprocess.run(
|
||||||
|
[
|
||||||
|
"pandoc",
|
||||||
|
"--standalone",
|
||||||
|
"--pdf-engine=xelatex",
|
||||||
|
"--variable=geometry:textwidth=10cm",
|
||||||
|
"--variable=monofont:Libertinus Mono",
|
||||||
|
"--include-in-header",
|
||||||
|
str(THESIS / "header-includes.tex"),
|
||||||
|
"--output",
|
||||||
|
str(output),
|
||||||
|
],
|
||||||
|
# Shift the identifier along the line to expose both forced splitting
|
||||||
|
# and font-dependent automatic hyphenation near the right margin.
|
||||||
|
input="\n\n".join(
|
||||||
|
f"{'word ' * count}`{identifier}`." for count in range(1, 16)
|
||||||
|
),
|
||||||
|
text=True,
|
||||||
|
encoding="utf-8",
|
||||||
|
capture_output=True,
|
||||||
|
check=False,
|
||||||
|
timeout=90,
|
||||||
|
)
|
||||||
|
assert result.returncode == 0, result.stderr
|
||||||
|
rendered = subprocess.run(
|
||||||
|
["pdftotext", "-layout", str(output), "-"],
|
||||||
|
capture_output=True,
|
||||||
|
text=True,
|
||||||
|
encoding="utf-8",
|
||||||
|
check=True,
|
||||||
|
timeout=15,
|
||||||
|
).stdout
|
||||||
|
assert rendered.count(identifier) == 15
|
||||||
|
|||||||
@@ -0,0 +1,28 @@
|
|||||||
|
"""Exercise the comparison snippet when the optional LangGraph library exists."""
|
||||||
|
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
("text", "expected_report"),
|
||||||
|
[(" Notes ", "# Report\n\nNotes"), (" ", "")],
|
||||||
|
)
|
||||||
|
def test_langgraph_comparison_routes_updated_state(
|
||||||
|
text: str, expected_report: str
|
||||||
|
) -> None:
|
||||||
|
"""Check the rendered example without making LangGraph a product dependency."""
|
||||||
|
pytest.importorskip("langgraph.graph")
|
||||||
|
manuscript = (
|
||||||
|
Path(__file__).resolve().parents[2]
|
||||||
|
/ "docs/thesis/system-design-implementation.md"
|
||||||
|
).read_text(encoding="utf-8")
|
||||||
|
section = manuscript.split("<!-- langgraph-comparison -->", 1)[1]
|
||||||
|
snippet = section.split("```python\n", 1)[1].split("```", 1)[0]
|
||||||
|
namespace = {}
|
||||||
|
# This is trusted repository prose, executed exactly as displayed.
|
||||||
|
exec(compile(snippet, "thesis-langgraph-example", "exec"), namespace)
|
||||||
|
result = namespace["graph"].invoke({"text": text, "report": ""})
|
||||||
|
assert result["text"] == text.strip()
|
||||||
|
assert result["report"] == expected_report
|
||||||
@@ -10,9 +10,11 @@ from typing import Any, cast
|
|||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from wf_client import App, Deployment, Run
|
from wf_api.durable_context import durable_workflow_api
|
||||||
|
from wf_client import App, Deployment, Run, Schedule
|
||||||
from wf_client.protocols import WorkflowClientPort
|
from wf_client.protocols import WorkflowClientPort
|
||||||
from wf_config import load_workflow_config
|
from wf_config import load_workflow_config
|
||||||
|
from wf_scheduling.store import FileScheduleStore
|
||||||
from wf_server.config import build_workflow_server_from_workflow_config
|
from wf_server.config import build_workflow_server_from_workflow_config
|
||||||
|
|
||||||
ROOT = Path(__file__).resolve().parents[2]
|
ROOT = Path(__file__).resolve().parents[2]
|
||||||
@@ -32,7 +34,11 @@ async def test_thesis_python_session_preserves_report_and_repair(
|
|||||||
config = load_workflow_config(ROOT / "examples/report_workflow/wf.config.json")
|
config = load_workflow_config(ROOT / "examples/report_workflow/wf.config.json")
|
||||||
config.server.store.root = tmp_path / "store"
|
config.server.store.root = tmp_path / "store"
|
||||||
server = build_workflow_server_from_workflow_config(config)
|
server = build_workflow_server_from_workflow_config(config)
|
||||||
app = App._from_port(cast(WorkflowClientPort, server.api))
|
# Enable real schedule persistence without starting a background scheduler:
|
||||||
|
# this session verifies registration, while another test exercises dispatch.
|
||||||
|
schedule_store = FileScheduleStore(config.server.store.root)
|
||||||
|
api = durable_workflow_api(server.context, schedule_store=schedule_store)
|
||||||
|
app = App._from_port(cast(WorkflowClientPort, api))
|
||||||
monkeypatch.chdir(ROOT)
|
monkeypatch.chdir(ROOT)
|
||||||
|
|
||||||
manuscript = THESIS.read_text(encoding="utf-8")
|
manuscript = THESIS.read_text(encoding="utf-8")
|
||||||
@@ -84,3 +90,14 @@ async def test_thesis_python_session_preserves_report_and_repair(
|
|||||||
for displayed in displayed_outputs:
|
for displayed in displayed_outputs:
|
||||||
assert displayed in diagnostic, "Displayed output differs from the session"
|
assert displayed in diagnostic, "Displayed output differs from the session"
|
||||||
assert namespace["trace"].frames
|
assert namespace["trace"].frames
|
||||||
|
schedule = namespace["schedule"]
|
||||||
|
assert isinstance(schedule, Schedule)
|
||||||
|
assert schedule.deployment_id == deployment.deployment_id
|
||||||
|
assert schedule.max_steps == 100
|
||||||
|
assert schedule.trigger["kind"] == "oneshot"
|
||||||
|
persisted = schedule_store.get_schedule(schedule.schedule_id)
|
||||||
|
assert persisted is not None
|
||||||
|
expression = persisted.input_bindings[0].expression
|
||||||
|
assert expression.kind == "literal"
|
||||||
|
assert expression.value == namespace["notes"]
|
||||||
|
assert namespace["history"]["occurrences"] == []
|
||||||
|
|||||||
Reference in New Issue
Block a user