From 0158e33da7586a9f261c7cfb1075871df2564c25 Mon Sep 17 00:00:00 2001 From: QuantumGhost Date: Mon, 29 Jun 2026 20:16:43 +0800 Subject: [PATCH 1/3] docs: introduce ADR mechanism --- .github/pull_request_template.md | 7 + CONTRIBUTING.md | 8 ++ README.md | 4 + ...001-adopt-architecture-decision-records.md | 79 +++++++++++ docs/adr/README.md | 10 ++ docs/adr/backlog.md | 29 ++++ scripts/new-adr.py | 124 ++++++++++++++++++ tests/test_new_adr_script.py | 83 ++++++++++++ 8 files changed, 344 insertions(+) create mode 100644 docs/adr/0001-adopt-architecture-decision-records.md create mode 100644 docs/adr/README.md create mode 100644 docs/adr/backlog.md create mode 100755 scripts/new-adr.py create mode 100644 tests/test_new_adr_script.py diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index d52b7441..5281a2f2 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -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 diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index d273567f..7b3ad41e 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -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 @@ -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 diff --git a/README.md b/README.md index 0b6a8498..4950d420 100644 --- a/README.md +++ b/README.md @@ -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): diff --git a/docs/adr/0001-adopt-architecture-decision-records.md b/docs/adr/0001-adopt-architecture-decision-records.md new file mode 100644 index 00000000..3b2c9011 --- /dev/null +++ b/docs/adr/0001-adopt-architecture-decision-records.md @@ -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. diff --git a/docs/adr/README.md b/docs/adr/README.md new file mode 100644 index 00000000..c55d1118 --- /dev/null +++ b/docs/adr/README.md @@ -0,0 +1,10 @@ +# 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) diff --git a/docs/adr/backlog.md b/docs/adr/backlog.md new file mode 100644 index 00000000..0a906594 --- /dev/null +++ b/docs/adr/backlog.md @@ -0,0 +1,29 @@ +# 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. + +## Current Priority + +### HITL interface + +- Related PRs: + [feat(human-input)!: extract Dify logic from HITL node](https://github.com/langgenius/graphon/pull/185) +- Why it needs an ADR: + this change is a clear Graphon-owned boundary decision. It removes + Dify-specific form entities and logic from core Graphon and redefines what + the human-input node interface is responsible for. + +### LLM Polling + +- Related PRs: + [feat: add LLM polling runtime support](https://github.com/langgenius/graphon/pull/151) +- Why it needs an ADR: + this introduces a new execution model for asynchronous LLM completion and is + one of the clearest Graphon-native runtime decisions worth documenting early. diff --git a/scripts/new-adr.py b/scripts/new-adr.py new file mode 100755 index 00000000..ef7fde52 --- /dev/null +++ b/scripts/new-adr.py @@ -0,0 +1,124 @@ +#!/usr/bin/env python3 +from __future__ import annotations + +import argparse +import re +import sys +from datetime import UTC, datetime +from pathlib import Path + +ADR_DIR = Path("docs/adr") +ADR_README = ADR_DIR / "README.md" +ADR_PATTERN = re.compile(r"^(?P\d{4,})-(?P[a-z0-9-]+)\.md$") + + +def parse_args() -> argparse.Namespace: + parser = argparse.ArgumentParser(description="Create a new ADR scaffold.") + parser.add_argument("--id", type=int, help="Explicit ADR id to use.") + parser.add_argument("--title", required=True, help="ADR title.") + return parser.parse_args() + + +def slugify(title: str) -> str: + slug = re.sub(r"[^a-z0-9]+", "-", title.lower()).strip("-") + if not slug: + msg = "title must contain at least one alphanumeric character" + raise ValueError(msg) + return slug + + +def next_adr_id(adr_dir: Path) -> int: + max_id = 0 + for path in adr_dir.iterdir(): + match = ADR_PATTERN.match(path.name) + if match is None: + continue + max_id = max(max_id, int(match.group("id"))) + return max_id + 1 + + +def format_adr_id(raw_id: int) -> str: + return f"{raw_id:04d}" + + +def render_adr(title: str, adr_id: str) -> str: + return f"""# ADR {adr_id}: {title} + +- Status: Proposed +- Date: {datetime.now(tz=UTC).date().isoformat()} +- Related PRs: N/A +- Supersedes: N/A +- Superseded by: N/A + +## Context + +Describe the pressure, problem, or ambiguity that forced this decision. + +## Decision + +Describe the decision in concrete terms. Prefer stable boundaries and contracts +over implementation trivia. + +## Consequences + +List the direct consequences of the decision, including migration cost, +limitations, and follow-up work. + +## Alternatives Considered + +Record the reasonable alternatives that were considered and rejected, and why +they were rejected. Do not try to enumerate every possible option. + +## Rollout Notes + +Record migration steps, compatibility notes, or deferred work if they matter to +future maintainers. +""" + + +def update_readme(readme_path: Path, filename: str, title: str) -> None: + readme = readme_path.read_text(encoding="utf-8") + marker = "## Historical Backfill" + if marker not in readme: + msg = f"could not find '{marker}' in {readme_path}" + raise ValueError(msg) + + entry = f"- [{filename}]({filename}): {title}\n\n" + if entry.strip() in readme: + return + + readme = readme.replace(marker, entry + marker, 1) + readme_path.write_text(readme, encoding="utf-8") + + +def main() -> int: + args = parse_args() + if not ADR_DIR.is_dir(): + msg = f"ADR directory not found: {ADR_DIR}" + raise SystemExit(msg) + if not ADR_README.is_file(): + msg = f"ADR README not found: {ADR_README}" + raise SystemExit(msg) + + slug = slugify(args.title) + raw_id = args.id if args.id is not None else next_adr_id(ADR_DIR) + adr_id = format_adr_id(raw_id) + filename = f"{adr_id}-{slug}.md" + output_path = ADR_DIR / filename + + if output_path.exists(): + msg = f"ADR already exists: {output_path}" + raise SystemExit(msg) + + output_path.write_text(render_adr(args.title, adr_id), encoding="utf-8") + update_readme(ADR_README, filename, args.title) + sys.stdout.write(f"{output_path}\n") + return 0 + + +if __name__ == "__main__": + try: + raise SystemExit(main()) + except ValueError as exc: + sys.stderr.write(f"{exc}\n") + raise SystemExit(1) from exc diff --git a/tests/test_new_adr_script.py b/tests/test_new_adr_script.py new file mode 100644 index 00000000..29d72d33 --- /dev/null +++ b/tests/test_new_adr_script.py @@ -0,0 +1,83 @@ +from __future__ import annotations + +import subprocess # noqa: S404 +import sys +from pathlib import Path + +SCRIPT_PATH = Path(__file__).resolve().parents[1] / "scripts" / "new-adr.py" + + +def write_adr_readme(tmp_path: Path) -> None: + adr_dir = tmp_path / "docs" / "adr" + adr_dir.mkdir(parents=True) + (adr_dir / "README.md").write_text( + """# Architecture Decision Records + +## Current ADRs + +- [0001-existing.md](0001-existing.md): existing decision + +## Historical Backfill + +- [backlog.md](backlog.md): grouped historical pull requests +""", + encoding="utf-8", + ) + (adr_dir / "0001-existing.md").write_text( + "# ADR 0001: Existing\n", + encoding="utf-8", + ) + + +def run_script(tmp_path: Path, *args: str) -> subprocess.CompletedProcess[str]: + return subprocess.run( # noqa: S603 + [sys.executable, str(SCRIPT_PATH), *args], + cwd=tmp_path, + capture_output=True, + text=True, + check=False, + ) + + +def test_requires_title_argument(tmp_path: Path) -> None: + write_adr_readme(tmp_path) + + result = run_script(tmp_path) + + assert result.returncode != 0 + assert "--title" in result.stderr + + +def test_assigns_next_id_and_updates_readme(tmp_path: Path) -> None: + write_adr_readme(tmp_path) + + result = run_script(tmp_path, "--title", "Add polling support") + + assert result.returncode == 0, result.stderr + new_adr_path = tmp_path / "docs" / "adr" / "0002-add-polling-support.md" + assert new_adr_path.exists() + new_adr = new_adr_path.read_text(encoding="utf-8") + assert "# ADR 0002: Add polling support" in new_adr + assert ( + "Record the reasonable alternatives that were considered and rejected," + in new_adr + ) + + readme = (tmp_path / "docs" / "adr" / "README.md").read_text(encoding="utf-8") + assert ( + "- [0002-add-polling-support.md](0002-add-polling-support.md): " + "Add polling support" + ) in readme + + +def test_uses_explicit_id_when_provided(tmp_path: Path) -> None: + write_adr_readme(tmp_path) + + result = run_script(tmp_path, "--id", "1234", "--title", "Extract HITL interface") + + assert result.returncode == 0, result.stderr + new_adr_path = tmp_path / "docs" / "adr" / "1234-extract-hitl-interface.md" + assert new_adr_path.exists() + assert "# ADR 1234: Extract HITL interface" in new_adr_path.read_text( + encoding="utf-8" + ) From fecb0f90f41aba50bfe034d90013a6f93f95980f Mon Sep 17 00:00:00 2001 From: QuantumGhost Date: Mon, 29 Jun 2026 21:11:57 +0800 Subject: [PATCH 2/3] fix(scripts): fix bugs in new-adr.py --- scripts/new-adr.py | 26 ++++++++++++++++++++------ tests/test_new_adr_script.py | 12 +++++++----- 2 files changed, 27 insertions(+), 11 deletions(-) diff --git a/scripts/new-adr.py b/scripts/new-adr.py index ef7fde52..eb44b6d9 100755 --- a/scripts/new-adr.py +++ b/scripts/new-adr.py @@ -4,7 +4,7 @@ import argparse import re import sys -from datetime import UTC, datetime +from datetime import datetime, timezone from pathlib import Path ADR_DIR = Path("docs/adr") @@ -45,7 +45,7 @@ def render_adr(title: str, adr_id: str) -> str: return f"""# ADR {adr_id}: {title} - Status: Proposed -- Date: {datetime.now(tz=UTC).date().isoformat()} +- Date: {datetime.now(tz=timezone.utc).date().isoformat()} - Related PRs: N/A - Supersedes: N/A - Superseded by: N/A @@ -78,16 +78,30 @@ def render_adr(title: str, adr_id: str) -> str: def update_readme(readme_path: Path, filename: str, title: str) -> None: readme = readme_path.read_text(encoding="utf-8") - marker = "## Historical Backfill" - if marker not in readme: + marker = "## ADRs" + marker_index = readme.find(marker) + if marker_index == -1: msg = f"could not find '{marker}' in {readme_path}" raise ValueError(msg) - entry = f"- [{filename}]({filename}): {title}\n\n" + entry = f"- [{filename}]({filename}): {title}\n" if entry.strip() in readme: return - readme = readme.replace(marker, entry + marker, 1) + insert_at = readme.find("\n## ", marker_index + len(marker)) + if insert_at == -1: + insert_at = len(readme) + + section = readme[marker_index:insert_at] + if not section.endswith("\n"): + section += "\n" + if not section.endswith("\n\n"): + section += "\n" + updated_section = section + entry + if not updated_section.endswith("\n"): + updated_section += "\n" + + readme = readme[:marker_index] + updated_section + readme[insert_at:] readme_path.write_text(readme, encoding="utf-8") diff --git a/tests/test_new_adr_script.py b/tests/test_new_adr_script.py index 29d72d33..ec6ffeb7 100644 --- a/tests/test_new_adr_script.py +++ b/tests/test_new_adr_script.py @@ -11,15 +11,16 @@ def write_adr_readme(tmp_path: Path) -> None: adr_dir = tmp_path / "docs" / "adr" adr_dir.mkdir(parents=True) (adr_dir / "README.md").write_text( - """# Architecture Decision Records + """# Architecture Decision Records Index -## Current ADRs +This directory stores Graphon's architecture decision records (ADRs). -- [0001-existing.md](0001-existing.md): existing decision +For the canonical ADR policy and workflow, see +[ADR 0001](0001-existing.md). -## Historical Backfill +## ADRs -- [backlog.md](backlog.md): grouped historical pull requests +- [0001-existing.md](0001-existing.md): existing decision """, encoding="utf-8", ) @@ -68,6 +69,7 @@ def test_assigns_next_id_and_updates_readme(tmp_path: Path) -> None: "- [0002-add-polling-support.md](0002-add-polling-support.md): " "Add polling support" ) in readme + assert "backlog.md" not in readme def test_uses_explicit_id_when_provided(tmp_path: Path) -> None: From 80229bf284509edfef5020b803b83600c46a5368 Mon Sep 17 00:00:00 2001 From: QuantumGhost Date: Mon, 29 Jun 2026 21:21:45 +0800 Subject: [PATCH 3/3] docs(adr): introduce ADR for HITL interface and LLM Polling --- .../0002-define-hitl-interface-boundary.md | 105 ++++++++++++++++++ ...0003-define-llm-polling-execution-model.md | 83 ++++++++++++++ docs/adr/README.md | 2 + docs/adr/backlog.md | 22 +--- 4 files changed, 195 insertions(+), 17 deletions(-) create mode 100644 docs/adr/0002-define-hitl-interface-boundary.md create mode 100644 docs/adr/0003-define-llm-polling-execution-model.md diff --git a/docs/adr/0002-define-hitl-interface-boundary.md b/docs/adr/0002-define-hitl-interface-boundary.md new file mode 100644 index 00000000..d0e932bd --- /dev/null +++ b/docs/adr/0002-define-hitl-interface-boundary.md @@ -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. diff --git a/docs/adr/0003-define-llm-polling-execution-model.md b/docs/adr/0003-define-llm-polling-execution-model.md new file mode 100644 index 00000000..cb5a32c5 --- /dev/null +++ b/docs/adr/0003-define-llm-polling-execution-model.md @@ -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. diff --git a/docs/adr/README.md b/docs/adr/README.md index c55d1118..9befc4e3 100644 --- a/docs/adr/README.md +++ b/docs/adr/README.md @@ -8,3 +8,5 @@ For the canonical ADR policy and workflow, see ## 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) diff --git a/docs/adr/backlog.md b/docs/adr/backlog.md index 0a906594..9ee5df28 100644 --- a/docs/adr/backlog.md +++ b/docs/adr/backlog.md @@ -9,21 +9,9 @@ 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. -## Current Priority +## Resolved -### HITL interface - -- Related PRs: - [feat(human-input)!: extract Dify logic from HITL node](https://github.com/langgenius/graphon/pull/185) -- Why it needs an ADR: - this change is a clear Graphon-owned boundary decision. It removes - Dify-specific form entities and logic from core Graphon and redefines what - the human-input node interface is responsible for. - -### LLM Polling - -- Related PRs: - [feat: add LLM polling runtime support](https://github.com/langgenius/graphon/pull/151) -- Why it needs an ADR: - this introduces a new execution model for asynchronous LLM completion and is - one of the clearest Graphon-native runtime decisions worth documenting early. +- 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)