From 94d86ea3a0d8bc504772e89c52a2c6208d484154 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sun, 16 Aug 2026 14:43:42 +0000 Subject: [PATCH 1/3] fix(ai): rebase adaptive defaults onto current main Keep ADR-0013 and drop the colliding ADR-0005 copies. Record one Unreleased changelog entry, restore the runtime-adapter and post-evaluation transport regressions, and correct the leftover post-chat docstring so it no longer describes a forced route. Co-authored-by: Seongho Bae --- CHANGELOG.md | 10 ++++ lineageweave/post_chat.py | 2 +- tests/test_adaptive_orchestrator_default.py | 49 +++++++++++++++++++ ..._contextual_orchestrator_default_policy.py | 40 +++++++++++++++ 4 files changed, 100 insertions(+), 1 deletion(-) create mode 100644 tests/test_adaptive_orchestrator_default.py create mode 100644 tests/test_contextual_orchestrator_default_policy.py diff --git a/CHANGELOG.md b/CHANGELOG.md index 0096828a..709e7688 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,16 @@ All notable changes to this project are documented here. Format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/); versioning follows [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [Unreleased] + +### Changed + +- Product LLM adapters now request contextual-orchestrator + `mode="auto"` rather than forcing a one-model route. The + orchestrator owns the quality-sufficient route, verification, or + conducted workflow; the explicit adjudication `verify` contract + remains unchanged. + ## [0.71.0] - 2026-08-14 ### Added diff --git a/lineageweave/post_chat.py b/lineageweave/post_chat.py index 7ec67e94..d23624bc 100644 --- a/lineageweave/post_chat.py +++ b/lineageweave/post_chat.py @@ -187,7 +187,7 @@ class ContextualOrchestratorPostChatClient: ``mode="verify"`` exists for (one worker call plus one checked verifier judgment), same reasoning ``adjudication_client`` already uses, not ``keyman_extraction``/``entity_relationship_classification``'s - single-pass ``mode="route"`` structured extraction. + single-pass ``mode="auto"`` structured extraction. """ available = True diff --git a/tests/test_adaptive_orchestrator_default.py b/tests/test_adaptive_orchestrator_default.py new file mode 100644 index 00000000..3de1dd41 --- /dev/null +++ b/tests/test_adaptive_orchestrator_default.py @@ -0,0 +1,49 @@ +"""LineageWeave delegates product-default LLM execution to auto policy.""" + +from __future__ import annotations + +from pathlib import Path + +from lineageweave import post_evaluation + + +def test_post_evaluation_adapter_defaults_to_auto(monkeypatch) -> None: + observed: dict[str, object] = {} + + def fake_post_json(url, payload, *, headers, timeout): + observed.update( + url=url, + payload=payload, + headers=headers, + timeout=timeout, + ) + return {"choices": [{"message": {"content": "{}"}}]} + + monkeypatch.setattr(post_evaluation, "post_json", fake_post_json) + adapter = post_evaluation._OrchestratorCompleteAdapter( + "https://orchestrator.example.test", "inference_token" + ) + adapter.complete([{"role": "user", "content": "Evaluate this evidence."}]) + + assert observed["payload"]["mode"] == "auto" + + +def test_post_evaluation_judge_uses_auto_by_default() -> None: + client = post_evaluation.ContextualOrchestratorPostEvaluationClient( + "https://orchestrator.example.test", "inference_token" + ) + assert client._judge.mode == "auto" + + +def test_runtime_clients_do_not_force_single_model_route() -> None: + package_root = Path(__file__).resolve().parents[1] / "lineageweave" + violations: list[str] = [] + for path in sorted(package_root.glob("*.py")): + text = path.read_text(encoding="utf-8") + if '"mode": "route"' in text or "'mode': 'route'" in text: + violations.append(f"{path.name}: request payload") + if 'mode="route"' in text or "mode='route'" in text: + violations.append(f"{path.name}: constructor/call default") + if 'mode: str = "route"' in text or "mode: str = 'route'" in text: + violations.append(f"{path.name}: typed default") + assert violations == [] diff --git a/tests/test_contextual_orchestrator_default_policy.py b/tests/test_contextual_orchestrator_default_policy.py new file mode 100644 index 00000000..6c5eb5d7 --- /dev/null +++ b/tests/test_contextual_orchestrator_default_policy.py @@ -0,0 +1,40 @@ +"""Contract tests for adaptive contextual-orchestrator consumer defaults.""" +from __future__ import annotations + +from pathlib import Path +import unittest + +ROOT = Path(__file__).resolve().parents[1] +ACTIVE_CLIENTS = ( + "lineageweave/post_summary.py", + "lineageweave/post_evaluation.py", + "lineageweave/keyman_extraction.py", + "lineageweave/commitment_extraction.py", + "lineageweave/post_chat.py", + "lineageweave/entity_relationship_classification.py", +) + + +class AdaptiveOrchestratorDefaultTest(unittest.TestCase): + """Protect production clients from regressing to forced one-model routing.""" + + def test_active_clients_use_auto_and_never_force_route(self) -> None: + for relative in ACTIVE_CLIENTS: + source = (ROOT / relative).read_text(encoding="utf-8") + with self.subTest(path=relative): + self.assertNotIn('"mode": "route"', source) + self.assertNotIn('mode="route"', source) + self.assertNotIn('mode: str = "route"', source) + self.assertTrue( + '"mode": "auto"' in source + or 'mode="auto"' in source + or 'mode: str = "auto"' in source + ) + + def test_high_stakes_adjudication_retains_explicit_checked_override(self) -> None: + source = (ROOT / "lineageweave/adjudication_client.py").read_text(encoding="utf-8") + self.assertIn('"mode": "verify"', source) + + +if __name__ == "__main__": + unittest.main() From 9ff7ad1cc0192a427ffc58b1cd861ac2f434157d Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sun, 16 Aug 2026 14:59:07 +0000 Subject: [PATCH 2/3] test(ai): lock auto and verify orchestrator transport contracts Keep post-chat and adjudication off the auto source-scan list so a docstring mention cannot satisfy the product default. Assert both checked-judgment clients send mode=verify on the wire. Co-authored-by: Seongho Bae --- ARCHITECTURE.md | 14 ++-- CHANGELOG.md | 4 +- tests/test_adaptive_orchestrator_default.py | 64 +++++++++++++++++- ..._contextual_orchestrator_default_policy.py | 65 +++++++++++++++---- 4 files changed, 123 insertions(+), 24 deletions(-) diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index 617b8b95..094a0aee 100644 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -83,17 +83,17 @@ flowchart LR | `server.py` | Stdlib HTTP server: `GET /api/lineage` (JSON graph) + static viewer | | `web/index.html` | Self-contained SVG DAG viewer, no build step, no external script dependency | -> **Known local-test-environment limitation:** `adjudication_client.py`'s -> `mode="verify"` call depends on contextual-orchestrator's -> `TaskOrchestrator.route_and_verify`, which as of this writing is still -> an open, unmerged upstream PR +> **Known local-test-environment limitation:** `adjudication_client.py` +> and `post_chat.py` send `mode="verify"` (ADR-0013). That call depends +> on contextual-orchestrator's `TaskOrchestrator.route_and_verify`, +> which as of this writing is still an open, unmerged upstream PR > (`ContextualWisdomLab/contextual-orchestrator#149`). Until it merges, -> the four adjudication/chat tests that exercise `mode="verify"` against +> the live adjudication/chat tests that exercise `mode="verify"` against > a real orchestrator fail with `invalid_mode` (the deployed `main` only > accepts `auto`/`route`/`conduct`) -- confirmed by reproducing the same > `400` directly against the orchestrator's own `/v1/chat/completions`, -> not caused by anything in this repo. `mode="route"` (every other -> pluggable client) is unaffected. +> not caused by anything in this repo. Ordinary product adapters request +> `mode="auto"` and are unaffected. ## Design decisions worth naming diff --git a/CHANGELOG.md b/CHANGELOG.md index 709e7688..d4b13c04 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,8 +11,8 @@ All notable changes to this project are documented here. Format follows - Product LLM adapters now request contextual-orchestrator `mode="auto"` rather than forcing a one-model route. The orchestrator owns the quality-sufficient route, verification, or - conducted workflow; the explicit adjudication `verify` contract - remains unchanged. + conducted workflow. Citation-bearing post-chat and lineage + adjudication keep their explicit `verify` contracts. ## [0.71.0] - 2026-08-14 diff --git a/tests/test_adaptive_orchestrator_default.py b/tests/test_adaptive_orchestrator_default.py index 3de1dd41..6b88197d 100644 --- a/tests/test_adaptive_orchestrator_default.py +++ b/tests/test_adaptive_orchestrator_default.py @@ -4,7 +4,8 @@ from pathlib import Path -from lineageweave import post_evaluation +from lineageweave import adjudication_client, post_chat, post_evaluation +from lineageweave.post_chat import ChatSourceDocument, ContextualOrchestratorPostChatClient def test_post_evaluation_adapter_defaults_to_auto(monkeypatch) -> None: @@ -35,6 +36,67 @@ def test_post_evaluation_judge_uses_auto_by_default() -> None: assert client._judge.mode == "auto" +def test_post_chat_requests_verify_mode(monkeypatch) -> None: + """Citation chat must send verify on the wire, not a docstring mention of auto.""" + + observed: dict[str, object] = {} + + def fake_post_json(url, payload, *, headers, timeout): + observed["payload"] = payload + return { + "choices": [ + { + "message": { + "content": ( + '{"answer_text": "The follow-up names the same bid.",' + ' "cited_source_numbers": [1]}' + ) + } + } + ] + } + + monkeypatch.setattr(post_chat, "post_json", fake_post_json) + client = ContextualOrchestratorPostChatClient( + "https://orchestrator.example.test", "inference_token" + ) + answer = client.answer( + "What happened between these events?", + [ + ChatSourceDocument( + post_id="post-bid-follow-up", + post_title="Bid follow-up", + post_body="Northridge asked to confirm the bid date.", + ) + ], + ) + + assert answer.cited_post_ids == ("post-bid-follow-up",) + assert observed["payload"]["mode"] == "verify" + + +def test_adjudication_requests_verify_mode(monkeypatch) -> None: + """Lineage adjudication must send verify on the wire, not a source substring.""" + + observed: dict[str, object] = {} + + def fake_post_json(url, payload, *, headers, timeout): + observed["payload"] = payload + return {"choices": [{"message": {"content": "0.91"}}]} + + monkeypatch.setattr(adjudication_client, "post_json", fake_post_json) + client = adjudication_client.ContextualOrchestratorAdjudicationClient( + "https://orchestrator.example.test", "inference_token" + ) + confidence = client.judge( + "Quarterly budget review meeting notes", + "Budget review follow-up: revised quarterly numbers", + ) + + assert confidence == 0.91 + assert observed["payload"]["mode"] == "verify" + + def test_runtime_clients_do_not_force_single_model_route() -> None: package_root = Path(__file__).resolve().parents[1] / "lineageweave" violations: list[str] = [] diff --git a/tests/test_contextual_orchestrator_default_policy.py b/tests/test_contextual_orchestrator_default_policy.py index 6c5eb5d7..24a023e1 100644 --- a/tests/test_contextual_orchestrator_default_policy.py +++ b/tests/test_contextual_orchestrator_default_policy.py @@ -1,39 +1,76 @@ """Contract tests for adaptive contextual-orchestrator consumer defaults.""" + from __future__ import annotations from pathlib import Path import unittest ROOT = Path(__file__).resolve().parents[1] -ACTIVE_CLIENTS = ( +AUTO_CLIENTS = ( "lineageweave/post_summary.py", "lineageweave/post_evaluation.py", "lineageweave/keyman_extraction.py", "lineageweave/commitment_extraction.py", - "lineageweave/post_chat.py", "lineageweave/entity_relationship_classification.py", ) +VERIFY_CLIENTS = ( + "lineageweave/post_chat.py", + "lineageweave/adjudication_client.py", +) +_ROUTE_MARKERS = ( + '"mode": "route"', + 'mode="route"', + 'mode: str = "route"', +) +_AUTO_MARKERS = ( + '"mode": "auto"', + 'mode="auto"', + 'mode: str = "auto"', +) +_VERIFY_MARKERS = ( + '"mode": "verify"', + 'mode="verify"', + 'mode: str = "verify"', +) + + +def _source(relative: str) -> str: + return (ROOT / relative).read_text(encoding="utf-8") + + +def _contains_any(source: str, markers: tuple[str, ...]) -> bool: + return any(marker in source for marker in markers) class AdaptiveOrchestratorDefaultTest(unittest.TestCase): """Protect production clients from regressing to forced one-model routing.""" - def test_active_clients_use_auto_and_never_force_route(self) -> None: - for relative in ACTIVE_CLIENTS: - source = (ROOT / relative).read_text(encoding="utf-8") + def test_auto_clients_request_auto_and_never_force_route(self) -> None: + for relative in AUTO_CLIENTS: + source = _source(relative) with self.subTest(path=relative): - self.assertNotIn('"mode": "route"', source) - self.assertNotIn('mode="route"', source) - self.assertNotIn('mode: str = "route"', source) + for marker in _ROUTE_MARKERS: + self.assertNotIn(marker, source) self.assertTrue( - '"mode": "auto"' in source - or 'mode="auto"' in source - or 'mode: str = "auto"' in source + _contains_any(source, _AUTO_MARKERS), + f"{relative} must request mode=auto in executable source", ) - def test_high_stakes_adjudication_retains_explicit_checked_override(self) -> None: - source = (ROOT / "lineageweave/adjudication_client.py").read_text(encoding="utf-8") - self.assertIn('"mode": "verify"', source) + def test_verify_clients_keep_checked_judgment_and_never_force_route(self) -> None: + for relative in VERIFY_CLIENTS: + source = _source(relative) + with self.subTest(path=relative): + for marker in _ROUTE_MARKERS: + self.assertNotIn(marker, source) + self.assertTrue( + _contains_any(source, _VERIFY_MARKERS), + f"{relative} must request mode=verify in executable source", + ) + self.assertNotIn( + '"mode": "auto"', + source, + f"{relative} must send verify, not a payload-level auto default", + ) if __name__ == "__main__": From 1b00bf407e1ee9a42164ef77c1d2b7da8584656a Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sun, 16 Aug 2026 15:00:20 +0000 Subject: [PATCH 3/3] docs: describe Keyman extraction as auto, not forced route The live client already sends mode=auto. Keep the research note aligned with ADR-0013 so operators do not reintroduce route. Co-authored-by: Seongho Bae --- docs/lineage-bi-research-notes.md | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/docs/lineage-bi-research-notes.md b/docs/lineage-bi-research-notes.md index acd55620..07079230 100644 --- a/docs/lineage-bi-research-notes.md +++ b/docs/lineage-bi-research-notes.md @@ -254,9 +254,10 @@ classified into the closed `{our_side, counterparty}` set is dropped rather than guessed. N:N organization attachments are slot-filling on that mention (a person may have zero, one, or several affiliations in the same post), not a second independent NER pass. The live client -calls contextual-orchestrator (`mode="route"`) rather than a raw LLM -API so reasoning-effort allocation stays centralized with the -adjudication channel. Proven for real during development against +calls contextual-orchestrator (`mode="auto"`) rather than a raw LLM +API so the orchestration plane can allocate route, verify, or a +deeper workflow; adjudication and post-chat keep explicit +`mode="verify"`. Proven for real during development against `fixtures.ambiguous_keyman_post` when orchestrator credentials are set; the default suite asserts the parser and the never-fake null client.