Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions .github/pull_request_template.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,17 @@

Closes #

## ADR

See [`docs/adr/0001-adopt-architecture-decision-records.md`](../docs/adr/0001-adopt-architecture-decision-records.md)
for the ADR policy and workflow. Record the relevant ADR path here, or `N/A`
if ADR 0001 says none is needed:

## Summary

## Checklist

- [ ] This pull request links the issue it resolves or advances
- [ ] This pull request title follows Conventional Commits, and any breaking change is marked with `!`
- [ ] I filled in the ADR field using ADR 0001 as the source of truth
- [ ] If CLA Assistant prompted me, I signed [CLA.md](../CLA.md) in the pull request conversation
8 changes: 8 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -154,6 +154,12 @@ docs(contributing): clarify CI workflow
refactor(api)!: remove deprecated runtime entrypoint
```

## Architecture Decision Records

The canonical ADR policy and workflow live in
[`docs/adr/0001-adopt-architecture-decision-records.md`](docs/adr/0001-adopt-architecture-decision-records.md).
Use that document as the source of truth for ADR decisions and PR wording.

## Issues

Before you start implementation or open a new issue, search the existing open
Expand Down Expand Up @@ -188,6 +194,8 @@ When you open a pull request:
- use a Conventional Commits title, and mark breaking changes with `!`, because
the pull request title becomes the squash merge commit message
- link the related issue in the pull request body
- use the PR template ADR field to point to the relevant ADR path, or mark
`N/A` when ADR 0001 says none is needed
- follow [`.github/pull_request_template.md`](.github/pull_request_template.md)
exactly
- do not delete required headings or checklist items from the template; if a
Expand Down
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,10 @@ planned as a separate follow-up.
## Internal Docs

- [CONTRIBUTING.md](CONTRIBUTING.md): contributor workflow, CI, commit/PR rules
- [docs/adr/README.md](docs/adr/README.md):
architecture decision record index
- [docs/adr/0001-adopt-architecture-decision-records.md](docs/adr/0001-adopt-architecture-decision-records.md):
canonical ADR policy and workflow
- [examples/slim_llm/README.md](examples/slim_llm/README.md):
runnable Slim LLM example setup
- [src/graphon/model_runtime/README.md](src/graphon/model_runtime/README.md):
Expand Down
79 changes: 79 additions & 0 deletions docs/adr/0001-adopt-architecture-decision-records.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
# ADR 0001: Adopt Architecture Decision Records

- Status: Accepted
- Date: 2026-06-29
- Related PRs: N/A
- Supersedes: N/A
- Superseded by: N/A

## Context

Graphon is moving quickly, but some semantics need to become explicit. Pull
requests alone do not clearly distinguish between:

- semantics the project intentionally supports
- behavior that is merely historical, evolutionary, or implied by the current
code

## Decision

Graphon will use ADRs under `docs/adr/` to record intentional semantics.

ADR is lightweight same-PR project memory:

- it lands in the same pull request as the semantic change
- it states the supported contract, behavior, or boundary that future users
should rely on
- it records consequences and reasonable rejected alternatives
- it does not duplicate implementation detail or document behavior solely
because the current code happens to do it

Historical, accidental, or code-implied behavior is out of scope unless the
project is explicitly choosing to support it as intended semantics.

Only explicitly experimental APIs or behaviors may defer ADR capture until the
semantics stabilize or become supported.

## Workflow

Add or update an ADR when a pull request does any of the following:

- establishes a semantic or contract that downstream users should rely on
- changes or intentionally retires an existing supported semantic or contract
- defines a cross-cutting boundary or ownership model
- promotes an explicitly experimental behavior into supported behavior

An ADR is usually unnecessary for:

- bug fixes within an already accepted semantic
- local refactors that preserve behavior and boundaries
- dependency updates, releases, CI changes, and routine contributor tooling
changes
- documentation-only or test-only changes

When an ADR is needed:

1. Create or update it in the same pull request as the semantic change.
2. Run `./new-adr.py --title "Short title"` from the repository root.
3. Pass `--id NNNN` only when you intentionally need a specific ADR number.
4. Keep it short: record the decision, the intended semantic, consequences, and
reasonable rejected alternatives with their rejection rationale.
5. Reference the ADR path in the pull request body, or explicitly state why no
ADR is needed.

## Alternatives Considered

- Use pull request discussion only:
rejected because it does not clearly separate intentionally supported
semantics from historical or code-implied behavior.
- Require a separate ADR flow before implementation:
rejected because it adds process cost without improving the quality of the
recorded decision for a fast-moving project.

## Consequences

- Graphon should have fewer ADRs, focused on stable supported semantics.
- Important semantics changes are in scope even when the public API shape is
mostly unchanged.
- Contributors should treat ADR as the place where the project states what
future users are expected to rely on.
105 changes: 105 additions & 0 deletions docs/adr/0002-define-hitl-interface-boundary.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
# ADR 0002: Define HITL Interface Boundary

- Status: Accepted
- Date: 2026-06-29
- Related PRs: #185
- Supersedes: N/A
- Superseded by: N/A

## Context

Graphon was extracted from Dify, but `HumanInputNode` still carried host-facing
semantics that did not belong in the library boundary.

In the old shape, Graphon knew about form schemas, resolved default values,
action definitions, session-oriented payloads, and host-side restoration
behavior. That made the HITL boundary too large:

- Graphon had to change when the host product changed its HITL semantics.
- Pause payloads leaked host-facing form content and delivery-oriented data.
- Host applications and Graphon had to evolve in lockstep.
- Too many ordinary HITL feature changes required synchronized work in both
Graphon and Dify.

Graphon needs a smaller HITL contract: one that preserves control-flow
semantics while leaving product semantics to the embedding application.

## Decision

Graphon owns only the HITL control-flow state machine.

The supported Graphon-owned boundary is:

- `HumanInputNode` builds `HITLContext` and calls a host-provided
`HITLCallback`.
- The callback returns one of three control-flow decisions:
`PauseRequested`, `Completed`, or `Expired`.
- Graphon translates those decisions into its own node events and node run
results.
- `Completed.inputs` and `Completed.outputs` carry runtime `Segment` mappings,
not serialized host payloads.
- HITL pause reasons are reduced to the minimal host lookup key:
`session_id`, `node_id`, and `node_title`.

This is intentionally a decision callback boundary, not an event stream or
generic result boundary. Graphon only needs to know:

- whether this execution should pause
- which branch handle should be selected when it continues
- which runtime inputs and outputs should be handed to downstream nodes

The host application owns everything else, including:

- form schema and rendering semantics
- action schema and output schema semantics
- default value resolution and submission validation
- session storage and persistence
- delivery and recipient semantics
- submission restoration and replay enrichment
- product-specific action semantics

Historical or Dify-specific payload shape is not part of the supported Graphon
interface. Graphon may keep compatibility shims when decoding legacy payloads,
but those legacy payloads are not the intended contract.

## Consequences

- Graphon HITL behavior is now defined in terms of callback-driven
control-flow, not form persistence semantics.
- Dify or another embedding host is responsible for reconstructing persisted
values into runtime `Segment` values before resuming Graphon execution.
- Embedding applications must own session lookup, form reconstruction, and
submission semantics outside Graphon.
- Most future HITL product changes should happen in the embedding application,
not in Graphon.
- Future HITL evolution in Graphon should focus on control-flow semantics and
node result translation, not host UX or transport details.

## Alternatives Considered

- Keep Graphon-owned form schema and runtime binding abstractions:
rejected because host product semantics leaked into the library and forced
synchronized changes across host and Graphon.
- Use an event-style callback boundary:
rejected because HITL here is a single control-flow decision, not a stream of
intermediate events.
- Return generic `NodeRunResult` values from the callback:
rejected because `Completed` and `Expired` make the HITL boundary narrower
and more self-descriptive than a generic node result type.
- Return serialized JSON-like payloads instead of runtime `Segment` values:
rejected because value restoration belongs to the host, and Graphon should
consume runtime values rather than host serialization semantics.
- Keep rich pause payloads with form content and resolved defaults:
rejected because that makes Graphon responsible for host-side storage,
delivery, and replay semantics.
- Move all HITL behavior into the host and make Graphon unaware of HITL:
rejected because Graphon still needs to own the workflow control-flow
semantics of pause, completion, expiration, and selected edge handling.

## Rollout Notes

- PR #185 introduced the callback boundary and removed Dify-specific form
entities and runtime bindings from the supported Graphon interface.
- Legacy `human_input_required` pause reasons are still normalized when
deserializing persisted payloads, but that compatibility path is not the
canonical contract.
83 changes: 83 additions & 0 deletions docs/adr/0003-define-llm-polling-execution-model.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
# ADR 0003: Define LLM Polling Execution Model

- Status: Accepted
- Date: 2026-06-29
- Related PRs: #151, #184
- Supersedes: N/A
- Superseded by: N/A

## Context

Some LLM runtimes cannot return a terminal result from a single invoke call.
Instead, they return a pending state that must be checked until the job
finishes or fails.

Graphon therefore needed a supported execution model for polling-capable LLM
runtimes.

An early direction was to model polling through Graphon's suspension and
resumption path, similar to the HITL flow. That was attractive when Dify Cloud
was believed to enforce a ten-minute workflow runtime limit, because a
continuously running workflow seemed impossible there.

That assumption later changed:

- Dify Cloud workflow runtime had already been relaxed to one hour, so keeping
the workflow running was no longer blocked by the old ten-minute limit.
- Plugin execution on Dify Cloud is still limited to ten minutes per run, so
the polling loop cannot live inside the plugin itself.

With that constraint set, the simpler question became: should polling be
modeled as suspension, or as continued execution of a running Graphon LLM node?

## Decision

Graphon models LLM polling as continued execution of a running LLM node.

The supported polling contract is:

- polling is an optional capability layered on top of the normal `LLMProtocol`
path via `LLMPollingCapableProtocol`
- the runtime starts polling through `start_llm_polling(...)`
- while the job is still running, the runtime returns `LLMPollingResult` with
`status=RUNNING` plus `plugin_state`
- Graphon remains in control of the polling loop, waits between checks, and
continues polling through `check_llm_polling(plugin_state=...)`
- polling progress is surfaced through lightweight Graphon events
- non-polling LLM runtimes stay on the existing invoke path

Polling is not modeled as suspension or resumption, and the supported polling
protocol does not carry workflow or node identity fields such as
`workflow_run_id` or `node_id`.

## Consequences

- Graphon and the embedding host remain responsible for keeping the workflow
alive during polling.
- Polling cannot be delegated to a long-running plugin loop on Dify Cloud,
because plugin execution time remains separately constrained.
- Polling-capable runtimes only need to persist and round-trip `plugin_state`
across checks.
- Graphon owns timeout, retry cadence, and progress visibility around the
polling loop.

## Alternatives Considered

- Use Graphon suspension and resumption, similar to HITL:
rejected because it introduces a more complex protocol and lifecycle than the
problem requires, while the original ten-minute Dify Cloud workflow limit is
no longer the blocking constraint it was once thought to be.
- Keep polling inside the plugin runtime:
rejected because plugin execution on Dify Cloud is still limited to ten
minutes per run, so the plugin cannot safely own the long-running polling
loop.
- Replace the normal invoke path with polling for all LLM runtimes:
rejected because polling is only needed for a subset of runtimes, and
non-polling models should keep the simpler existing execution path.

## Rollout Notes

- PR #151 introduced the initial polling capability, polling result/config
entities, and progress events.
- PR #184 removed `workflow_run_id` and `node_id` from the public polling
protocol after the suspension-based route was rejected.
12 changes: 12 additions & 0 deletions docs/adr/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
# Architecture Decision Records Index

This directory stores Graphon's architecture decision records (ADRs).

For the canonical ADR policy and workflow, see
[ADR 0001](0001-adopt-architecture-decision-records.md).

## ADRs

- [ADR 0001: Adopt Architecture Decision Records](0001-adopt-architecture-decision-records.md)
- [ADR 0002: Define HITL Interface Boundary](0002-define-hitl-interface-boundary.md)
- [ADR 0003: Define LLM Polling Execution Model](0003-define-llm-polling-execution-model.md)
17 changes: 17 additions & 0 deletions docs/adr/backlog.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
# ADR Backlog

This backlog lists historical pull request groups that should be backfilled as
ADRs.

The unit of work is one ADR per durable decision, not one ADR per pull request.

Many older boundaries in this repository were extracted from Dify. For now,
this backlog intentionally keeps only the Graphon-specific decisions that are
most worth recording first. Other historical changes can be reviewed later.

## Resolved

- HITL interface:
captured by [ADR 0002](0002-define-hitl-interface-boundary.md)
- LLM Polling:
captured by [ADR 0003](0003-define-llm-polling-execution-model.md)
Loading
Loading