diff --git a/CHANGELOG.d/0.78.0-adaptive-orchestration.md b/CHANGELOG.d/0.78.0-adaptive-orchestration.md new file mode 100644 index 000000000..b51b63624 --- /dev/null +++ b/CHANGELOG.d/0.78.0-adaptive-orchestration.md @@ -0,0 +1,6 @@ +# 0.78.0 — Adaptive contextual-orchestrator defaults + +## Changed + +- Structured extraction, summarization, commitment, relationship-classification, and LLM-as-a-Judge consumers now request contextual-orchestrator `auto` mode, allowing the orchestration plane to meet the quality requirement and then minimize known execution cost. +- Explicit checked `verify` paths remain unchanged. diff --git a/docs/adr/0013-adaptive-contextual-orchestrator-default.md b/docs/adr/0013-adaptive-contextual-orchestrator-default.md new file mode 100644 index 000000000..ee0402075 --- /dev/null +++ b/docs/adr/0013-adaptive-contextual-orchestrator-default.md @@ -0,0 +1,28 @@ +# ADR-0013: Adaptive contextual-orchestrator mode is the default + +- Status: Accepted +- Date: 2026-08-16 + +## Context + +LineageWeave used fixed single-worker `route` mode for structured extraction, summarization, commitment derivation, relationship classification, and post evaluation. That made each consumer choose the execution topology and prevented `contextual-orchestrator` from allocating deeper verification when task difficulty, uncertainty, or risk justified it. + +## Decision + +Ordinary LineageWeave LLM consumers request `mode="auto"`. + +The orchestration plane owns provider/model selection, test-time compute, workflow depth, verification, fallback, and known-price optimization. Quality sufficiency is the first constraint; cost is minimized among execution paths that satisfy it. Unpriced models are not treated as free. + +Explicit `verify` remains for the citation-bearing post-chat and lineage adjudication paths because those are deliberate checked-judgment contracts, not product defaults. Explicit route or conduct modes may be used only for documented ablation, incident response, or a bounded domain requirement. + +LineageWeave continues to own strict output parsing, evidence identifiers, IRT projection, and fail-closed domain validation. + +## Consequences + +A structured task may still be served by one model when the adaptive policy determines that it is sufficient. Harder requests may receive a deeper workflow without changing the LineageWeave API. Consumers must retain returned orchestration and usage evidence when the gateway exposes it. + +## References + +Omidvar, H., & Akhlaghi, V. (2026). *A communication-theoretic framework for LLM agents: Cost-aware adaptive reliability* [Preprint]. arXiv. https://doi.org/10.48550/arXiv.2605.09121 + +Tang, Y., Cetin, E., Xu, J., Sun, Q., Nielsen, S., Richard, V., Goda, H., Tymchenko, I., Nguyen, N., Lee, H., Ashiga, M., Kotyan, S., Kuroki, S., & Clanuwat, T. (2026). *Sakana Fugu technical report* [Technical report]. arXiv. https://doi.org/10.48550/arXiv.2606.21228 diff --git a/lineageweave/commitment_extraction.py b/lineageweave/commitment_extraction.py index 341eaa0c8..459376617 100644 --- a/lineageweave/commitment_extraction.py +++ b/lineageweave/commitment_extraction.py @@ -136,7 +136,7 @@ def parse_commitment_response(content: str) -> CustomerCommitment | None: class ContextualOrchestratorCommitmentExtractionClient: - """Calls ``POST {base_url}/v1/chat/completions`` with ``mode="route"``.""" + """Calls ``POST {base_url}/v1/chat/completions`` with ``mode="auto"``.""" available = True @@ -156,7 +156,7 @@ def extract(self, post_title: str, post_body: str, reference_date: str) -> Custo f"{self._base_url}/v1/chat/completions", { "messages": [{"role": "user", "content": prompt}], - "mode": "route", + "mode": "auto", "reasoning_effort": self._reasoning_effort, }, headers={"authorization": f"Bearer {self._api_key}"}, diff --git a/lineageweave/entity_relationship_classification.py b/lineageweave/entity_relationship_classification.py index d1a2056ca..ad23e7d9a 100644 --- a/lineageweave/entity_relationship_classification.py +++ b/lineageweave/entity_relationship_classification.py @@ -157,7 +157,7 @@ def parse_classification_response( class ContextualOrchestratorEntityRelationshipClient: - """Calls ``POST {base_url}/v1/chat/completions`` with ``mode="route"``.""" + """Calls ``POST {base_url}/v1/chat/completions`` with ``mode="auto"``.""" available = True @@ -183,7 +183,7 @@ def classify( f"{self._base_url}/v1/chat/completions", { "messages": [{"role": "user", "content": prompt}], - "mode": "route", + "mode": "auto", "reasoning_effort": self._reasoning_effort, }, headers={"authorization": f"Bearer {self._api_key}"}, diff --git a/lineageweave/keyman_extraction.py b/lineageweave/keyman_extraction.py index d7e8de952..5775c0f20 100644 --- a/lineageweave/keyman_extraction.py +++ b/lineageweave/keyman_extraction.py @@ -11,10 +11,10 @@ contextual-orchestrator instance -- never a raw LLM API directly, per AGENTS.md -- because Keyman identification is a structured-extraction task that benefits from the orchestrator's reasoning-effort allocation, not a -single confidence number, so it uses ``mode="route"`` (one worker call) at -a ``"medium"`` reasoning effort by default rather than ``verify``'s -worker-plus-checker pattern, which is reserved for adjudication's binary -judgment calls. +single confidence number, so it uses ``mode="auto"`` and lets the orchestration plane allocate +quality-sufficient test-time compute before minimizing known cost. Explicit +``verify`` remains reserved for adjudication's checked binary-judgment +contract. """ from __future__ import annotations @@ -133,7 +133,7 @@ def parse_keyman_response(content: str) -> list[PersonMention]: class ContextualOrchestratorKeymanExtractionClient: - """Calls ``POST {base_url}/v1/chat/completions`` with ``mode="route"``.""" + """Calls ``POST {base_url}/v1/chat/completions`` with ``mode="auto"``.""" available = True @@ -151,7 +151,7 @@ def extract(self, post_title: str, post_body: str) -> list[PersonMention]: f"{self._base_url}/v1/chat/completions", { "messages": [{"role": "user", "content": prompt}], - "mode": "route", + "mode": "auto", "reasoning_effort": self._reasoning_effort, }, headers={"authorization": f"Bearer {self._api_key}"}, diff --git a/lineageweave/post_evaluation.py b/lineageweave/post_evaluation.py index 09ce21fd2..2d3be6291 100644 --- a/lineageweave/post_evaluation.py +++ b/lineageweave/post_evaluation.py @@ -85,7 +85,7 @@ def __init__(self, base_url: str, api_key: str, *, timeout: float = 60.0) -> Non self._api_key = api_key self._timeout = timeout - def complete(self, messages: list[dict[str, Any]], mode: str = "route") -> dict[str, Any]: + def complete(self, messages: list[dict[str, Any]], mode: str = "auto") -> dict[str, Any]: body = post_json( f"{self._base_url}/v1/chat/completions", {"messages": messages, "mode": mode, "reasoning_effort": "medium"}, @@ -107,7 +107,7 @@ class ContextualOrchestratorPostEvaluationClient: def __init__(self, base_url: str, api_key: str, *, timeout: float = 60.0) -> None: self._judge = ContextualOrchestratorJudge( _OrchestratorCompleteAdapter(base_url, api_key, timeout=timeout), - mode="route", + mode="auto", ) def evaluate(self, post_title: str, post_body: str) -> LLMJudgeResult: diff --git a/lineageweave/post_summary.py b/lineageweave/post_summary.py index 4974268c2..eac051fda 100644 --- a/lineageweave/post_summary.py +++ b/lineageweave/post_summary.py @@ -152,7 +152,7 @@ def parse_summary_response(content: str) -> PostSummary | None: class ContextualOrchestratorPostSummaryClient: - """Calls ``POST {base_url}/v1/chat/completions`` with ``mode="route"``.""" + """Calls ``POST {base_url}/v1/chat/completions`` with ``mode="auto"``.""" available = True @@ -170,7 +170,7 @@ def summarize(self, post_title: str, post_body: str) -> PostSummary: f"{self._base_url}/v1/chat/completions", { "messages": [{"role": "user", "content": prompt}], - "mode": "route", + "mode": "auto", "reasoning_effort": self._reasoning_effort, }, headers={"authorization": f"Bearer {self._api_key}"}, diff --git a/tests/test_adaptive_orchestration_defaults.py b/tests/test_adaptive_orchestration_defaults.py new file mode 100644 index 000000000..aa84ad30a --- /dev/null +++ b/tests/test_adaptive_orchestration_defaults.py @@ -0,0 +1,131 @@ +"""Adaptive contextual-orchestrator defaults remain explicit at every consumer boundary.""" + +from __future__ import annotations + +import json + +import pytest + +from lineageweave.commitment_extraction import ( + ContextualOrchestratorCommitmentExtractionClient, +) +from lineageweave.entity_relationship_classification import ( + ContextualOrchestratorEntityRelationshipClient, +) +from lineageweave.keyman_extraction import ( + ContextualOrchestratorKeymanExtractionClient, +) +from lineageweave.post_evaluation import ContextualOrchestratorPostEvaluationClient +from lineageweave.post_summary import ContextualOrchestratorPostSummaryClient + + +@pytest.mark.parametrize( + ("module_name", "client_factory", "invoke", "content"), + [ + ( + "lineageweave.post_summary", + lambda: ContextualOrchestratorPostSummaryClient("https://orchestrator.test", "token"), + lambda client: client.summarize("Title", "Body"), + json.dumps( + { + "korean_summary": "요약", + "key_events": [], + "roles_and_responsibilities": [], + } + ), + ), + ( + "lineageweave.keyman_extraction", + lambda: ContextualOrchestratorKeymanExtractionClient( + "https://orchestrator.test", "token" + ), + lambda client: client.extract("Title", "Body"), + "[]", + ), + ( + "lineageweave.commitment_extraction", + lambda: ContextualOrchestratorCommitmentExtractionClient( + "https://orchestrator.test", "token" + ), + lambda client: client.extract("Title", "Body", "2026-08-16"), + json.dumps( + { + "has_commitment": False, + "commitment_summary": None, + "due_date": None, + } + ), + ), + ( + "lineageweave.entity_relationship_classification", + lambda: ContextualOrchestratorEntityRelationshipClient( + "https://orchestrator.test", "token" + ), + lambda client: client.classify("Title", "Body", ["Example Corp"]), + json.dumps( + [ + { + "organization_name": "Example Corp", + "relationship_type_code": "rel_voc", + } + ] + ), + ), + ], +) +def test_structured_consumers_request_auto_mode( + monkeypatch, module_name, client_factory, invoke, content +) -> None: + observed: dict[str, object] = {} + + def fake_post_json(url, payload, *, headers, timeout): + observed["url"] = url + observed["payload"] = payload + observed["headers"] = headers + observed["timeout"] = timeout + return {"choices": [{"message": {"content": content}}]} + + module = __import__(module_name, fromlist=["post_json"]) + monkeypatch.setattr(module, "post_json", fake_post_json) + + invoke(client_factory()) + + assert observed["payload"]["mode"] == "auto" + + +def test_post_evaluation_judge_defaults_to_auto(monkeypatch) -> None: + observed: dict[str, object] = {} + + def fake_post_json(url, payload, *, headers, timeout): + observed["payload"] = payload + return { + "choices": [ + { + "message": { + "content": json.dumps( + { + "score": 1.0, + "accepted": True, + "rationale": "Evidence supports each criterion.", + "criterion_categories": { + "general_sentiment_negative": 4, + "general_sentiment_positive": 4, + "sales_lead_specificity": 4, + }, + } + ) + } + } + ] + } + + import lineageweave.post_evaluation as module + + monkeypatch.setattr(module, "post_json", fake_post_json) + client = ContextualOrchestratorPostEvaluationClient( + "https://orchestrator.test", "token" + ) + + client.evaluate("Title", "Body") + + assert observed["payload"]["mode"] == "auto"