feat(llm): scoped model runtime, history rebuild and gateway proxy streaming

This commit is contained in:
m4
2026-09-13 15:12:17 +08:00
parent 382ed305cc
commit 57bae2e6ae
11 changed files with 1656 additions and 54 deletions
+2
View File
@@ -398,6 +398,8 @@ class AdapterRegistration:
)
if reasoning not in {None, "off"}:
result["reasoning"] = {"effort": reasoning}
if self.adapter_id in {"openai", "xai"}:
result["reasoning"]["summary"] = "auto"
elif self.adapter_id == "dashscope" and thinking:
result["reasoning"] = {"effort": "medium"}
elif self.adapter_id == "google-gemini":
+48 -6
View File
@@ -68,6 +68,14 @@ def now_ms() -> int:
def _unsigned(value: Any) -> dict[str, Any]:
payload = asdict(value)
payload.pop("signature", None)
if payload.get("predecessor_owner_epoch") == 0:
payload.pop("predecessor_owner_epoch")
for name in ("continuation_pending_hash", "continuation_decision_hash"):
if payload.get(name) == "":
payload.pop(name)
for name in ("execution_id", "predecessor_execution_id", "predecessor_checkpoint_id"):
if payload.get(name) == "":
payload.pop(name)
return payload
@@ -95,6 +103,11 @@ class RoutePreparationGrant:
key_id: str
signature: str
contract_version: int = CONTRACT_VERSION
predecessor_execution_id: str = ""
predecessor_checkpoint_id: str = ""
predecessor_owner_epoch: int = 0
continuation_pending_hash: str = ""
continuation_decision_hash: str = ""
def unsigned_payload(self) -> dict[str, Any]:
return _unsigned(self)
@@ -195,6 +208,7 @@ class PreparedRunQuote:
key_id: str
signature: str
contract_version: int = CONTRACT_VERSION
execution_id: str = ""
def unsigned_payload(self) -> dict[str, Any]:
return _unsigned(self)
@@ -622,11 +636,7 @@ class HmacGrantAuthority:
payload = {**defaults, **kwargs}
if "roles" in payload:
payload["roles"] = tuple(sorted({str(role) for role in payload["roles"]}))
unsigned = {
key_name: value
for key_name, value in payload.items()
if key_name != "signature"
}
unsigned = _unsigned(contract_class(signature="", **payload))
signature = sign_contract(contract_class.__name__, unsigned, key)
return contract_class(signature=signature, **payload)
@@ -766,6 +776,22 @@ class AgentExecutionProfile:
return cls.web_v3()
@dataclass(frozen=True, slots=True)
class ModelFactoryResult:
"""Explicit transfer of exclusively owned closeable resources to one run.
Plain custom factory return values are borrowed. Do not list shared clients.
The runtime closes only these exact objects, never their reachable children.
Built-in factories register newly allocated transports during construction.
Cleanup tries every resource even after failures, at most three times per
resource. Failed resources and their exception history remain owned; no
successful terminal event is emitted until all resources close.
"""
model: Any
owned_clients: tuple[Any, ...] = ()
@dataclass(frozen=True, slots=True)
class AgentModelSet:
main_agent: Any
@@ -864,7 +890,23 @@ class EvoWebRun(Protocol):
self, after_sequence: int | None = None
) -> AsyncIterator[EvoRuntimeEvent]: ...
async def cancel(self, reason: str) -> str: ...
async def cancel(self, reason: str, *, owner_epoch: int | None = None,
boot_id: str | None = None) -> str: ...
async def wait_stopped(self, timeout: float | None = 2.0) -> str:
"""Return a terminal outcome only after owned resource cleanup.
A bounded timeout returns ``unknown`` without cancelling execution or
cleanup. Cancelling the waiter also leaves cleanup owned by the run.
Host checkpointers and workspace backends are borrowed, never closed.
``awaiting_input`` ends this execution, not the checkpoint workflow;
a separate run may resume it. Its run_terminal payload includes the
final checkpoint_thread_id, checkpoint_id, checkpoint_ns and JSON
pending_interrupts (id/value records). Consumers must not interpret
every non-failed outcome as completed. Final checkpoint read failure
yields failed with FINAL_CHECKPOINT_READ_FAILED, never completed.
"""
...
def event_payload(value: Any) -> dict[str, Any]:
+24 -1
View File
@@ -174,7 +174,30 @@ def _chunk_to_message(chunk: dict[str, Any]) -> BaseMessage:
parsed = messages_from_dict([message_dict])
if len(parsed) != 1:
raise RuntimeError("AI4SCI_MODEL_STREAM_DELTA_INVALID")
return parsed[0]
message = parsed[0]
summary = delta.get("reasoning_summary")
if isinstance(summary, str) and summary:
content = list(message.content) if isinstance(message.content, list) else []
existing_summary = "".join(
str(part.get("text") or "")
for block in content
if isinstance(block, Mapping) and block.get("type") == "reasoning"
for part in (block.get("summary") or [])
if isinstance(part, Mapping) and part.get("type") == "summary_text"
)
if summary.startswith(existing_summary):
summary_delta = summary[len(existing_summary):]
elif existing_summary.startswith(summary):
summary_delta = ""
else:
summary_delta = summary
if summary_delta:
content.append({
"type": "reasoning",
"summary": [{"type": "summary_text", "text": summary_delta}],
})
message.content = content
return message
def proxy_from_config(
+146 -9
View File
@@ -3,7 +3,9 @@
from __future__ import annotations
import json
import logging
from collections.abc import AsyncIterator, Mapping, Sequence
from types import SimpleNamespace
from typing import Any
from langchain_core.language_models.chat_models import BaseChatModel
@@ -20,6 +22,60 @@ from langchain_core.tools import BaseTool
from langchain_core.utils.function_calling import convert_to_openai_tool
from pydantic import Field, SecretStr
logger = logging.getLogger(__name__)
def _cleanup_outcome(
primary: BaseException | None, errors: list[BaseException]
) -> None:
if primary is not None or not errors:
return
for error in errors:
if not isinstance(error, Exception):
raise error
raise RuntimeError("MODEL_PROVIDER_CLEANUP_ERROR") from None
def _record_cleanup_error(resource: str) -> None:
# Never log exception messages, reprs or tracebacks containing provider secrets.
try:
logger.warning("MODEL_PROVIDER_CLEANUP_ERROR resource=%s", resource)
except Exception:
# A broken logging handler must not replace the provider exception either.
pass
def _close_owned(client: Any, stream: Any, primary: BaseException | None) -> None:
errors: list[BaseException] = []
for resource, target in (("stream", stream), ("client", client)):
if target is None:
continue
try:
target.close()
except BaseException as error:
errors.append(error)
_record_cleanup_error(resource)
_cleanup_outcome(primary, errors)
async def _aclose_owned(
client: Any, stream: Any, primary: BaseException | None
) -> None:
errors: list[BaseException] = []
for resource in ("stream", "async_client", "client"):
try:
if resource == "stream":
if stream is not None:
await stream.close()
elif resource == "async_client":
await client.aio.aclose()
else:
client.close()
except BaseException as error:
errors.append(error)
_record_cleanup_error(resource)
_cleanup_outcome(primary, errors)
class GeminiInteractionsChatModel(BaseChatModel):
"""Minimal native bridge that preserves signed Provider content blocks."""
@@ -96,7 +152,7 @@ class GeminiInteractionsChatModel(BaseChatModel):
"generation_config": generation_config,
"tools": list(self.bound_tools),
"store": False,
"stream": False,
"stream": True,
}
def _generate(
@@ -108,8 +164,20 @@ class GeminiInteractionsChatModel(BaseChatModel):
request = self._request(messages)
if stop:
request["generation_config"]["stop_sequences"] = stop
response = self._client().interactions.create(**request)
return _chat_result(response)
client = self._client()
stream = None
state = _InteractionAccumulator()
primary = None
try:
stream = client.interactions.create(**request)
for event in stream:
state.accept(_dump(event))
return state.result()
except BaseException as error:
primary = error
raise
finally:
_close_owned(client, stream, primary)
async def _agenerate(
self,
@@ -120,8 +188,20 @@ class GeminiInteractionsChatModel(BaseChatModel):
request = self._request(messages)
if stop:
request["generation_config"]["stop_sequences"] = stop
response = await self._client().aio.interactions.create(**request)
return _chat_result(response)
client = self._client()
stream = None
state = _InteractionAccumulator()
primary = None
try:
stream = await client.aio.interactions.create(**request)
async for event in stream:
state.accept(_dump(event))
return state.result()
except BaseException as error:
primary = error
raise
finally:
await _aclose_owned(client, stream, primary)
async def _astream(
self,
@@ -133,8 +213,22 @@ class GeminiInteractionsChatModel(BaseChatModel):
request["stream"] = True
if stop:
request["generation_config"]["stop_sequences"] = stop
stream = await self._client().aio.interactions.create(**request)
client = self._client()
stream = None
primary = None
try:
stream = await client.aio.interactions.create(**request)
async for chunk in self._astream_events(stream):
yield chunk
except BaseException as error:
primary = error
raise
finally:
await _aclose_owned(client, stream, primary)
async def _astream_events(self, stream: Any) -> AsyncIterator[ChatGenerationChunk]:
blocks: dict[int, dict[str, Any]] = {}
completed = False
async for event in stream:
payload = _dump(event)
event_type = payload.get("event_type")
@@ -175,6 +269,7 @@ class GeminiInteractionsChatModel(BaseChatModel):
if event_type == "error":
raise RuntimeError("MODEL_PROVIDER_PROTOCOL_ERROR")
if event_type == "interaction.complete":
completed = True
interaction = payload.get("interaction") or {}
ordered_blocks = [blocks[index] for index in sorted(blocks)]
yield ChatGenerationChunk(
@@ -188,9 +283,7 @@ class GeminiInteractionsChatModel(BaseChatModel):
provider_request_id=interaction.get("id"),
),
response_metadata={
"model_name": str(
(interaction.get("model") or {}).get("id") or ""
),
"model_name": _interaction_model_name(interaction),
"finish_reason": str(
interaction.get("status") or "unknown"
),
@@ -198,6 +291,50 @@ class GeminiInteractionsChatModel(BaseChatModel):
)
)
if not completed:
raise RuntimeError("MODEL_PROVIDER_PROTOCOL_ERROR")
class _InteractionAccumulator:
def __init__(self) -> None:
self.blocks: dict[int, dict[str, Any]] = {}
self.interaction: dict[str, Any] | None = None
def accept(self, payload: dict[str, Any]) -> None:
kind = payload.get("event_type")
if kind == "content.start":
self.blocks[int(payload["index"])] = dict(payload.get("content") or {})
elif kind == "content.delta":
_merge_stream_delta(
self.blocks.setdefault(int(payload["index"]), {}),
dict(payload.get("delta") or {}),
)
elif kind == "error":
raise RuntimeError("MODEL_PROVIDER_PROTOCOL_ERROR")
elif kind == "interaction.complete":
self.interaction = payload.get("interaction") or {}
def result(self) -> ChatResult:
if self.interaction is None:
raise RuntimeError("MODEL_PROVIDER_PROTOCOL_ERROR")
interaction = self.interaction
return _chat_result(
SimpleNamespace(
outputs=[self.blocks[index] for index in sorted(self.blocks)],
usage=interaction.get("usage"),
id=interaction.get("id"),
status=interaction.get("status", "unknown"),
model=SimpleNamespace(id=_interaction_model_name(interaction)),
)
)
def _interaction_model_name(interaction: Mapping[str, Any]) -> str:
model = interaction.get("model")
if isinstance(model, Mapping):
return str(model.get("id") or "")
return str(model or "")
def _message_content(message: BaseMessage) -> list[dict[str, Any]]:
if isinstance(message, AIMessage):
+112
View File
@@ -0,0 +1,112 @@
"""Local durable initialization gate. All graph access must use this gate.
The file lock covers graph writes as well as owner changes; a row CAS alone
cannot fence an old writer while it is suspended in a graph await.
"""
import asyncio
import fcntl
import sqlite3
from contextlib import asynccontextmanager, closing
from pathlib import Path
from langgraph.graph import START, END
class SqliteInitializationStore:
def __init__(self, path):
if str(path) == ":memory:":
raise ValueError("initialization requires a durable file")
self.path = str(Path(path).resolve())
with closing(self.connect()) as db, db:
db.execute("""CREATE TABLE IF NOT EXISTS history_initialization (
key TEXT PRIMARY KEY, attempt TEXT NOT NULL, digest TEXT NOT NULL,
owner TEXT NOT NULL, fence INTEGER NOT NULL, status TEXT NOT NULL)""")
def connect(self):
db = sqlite3.connect(self.path)
db.row_factory = sqlite3.Row
return db
def get(self, key):
with closing(self.connect()) as db:
row = db.execute("SELECT * FROM history_initialization WHERE key=?", (key,)).fetchone()
return dict(row) if row else None
@asynccontextmanager
async def locked(self, key):
with open(self.path + "." + key.split(":")[-1] + ".lock", "a") as lock:
while True:
try:
fcntl.flock(lock, fcntl.LOCK_EX | fcntl.LOCK_NB)
break
except BlockingIOError:
await asyncio.sleep(0.01)
try:
yield
finally:
fcntl.flock(lock, fcntl.LOCK_UN)
def claim(self, key, attempt, digest, owner, fence):
if not isinstance(attempt, str) or not attempt.strip() or not isinstance(owner, str) or not owner.strip():
raise ValueError("attempt and owner required")
if type(fence) is not int or fence < 1:
raise ValueError("positive owner fence required")
with closing(self.connect()) as db, db:
db.execute("BEGIN IMMEDIATE")
row = db.execute("SELECT * FROM history_initialization WHERE key=?", (key,)).fetchone()
if row:
if row["attempt"] != attempt or row["digest"] != digest:
raise ValueError("history attempt/content conflict")
if fence < row["fence"] or (fence == row["fence"] and owner != row["owner"]):
raise ValueError("stale history owner fence")
if row["status"] not in ("INITIALIZING", "READY"):
raise ValueError("history checkpoint already consumed")
db.execute("UPDATE history_initialization SET owner=?, fence=? WHERE key=?", (owner, fence, key))
else:
db.execute("INSERT INTO history_initialization VALUES (?,?,?,?,?,?)",
(key, attempt, digest, owner, fence, "INITIALIZING"))
def status(self, key, attempt, owner, fence, before, after):
with closing(self.connect()) as db, db:
changed = db.execute("""UPDATE history_initialization SET status=?
WHERE key=? AND attempt=? AND owner=? AND fence=? AND status=?""",
(after, key, attempt, owner, fence, before)).rowcount
if changed != 1:
raise ValueError("history requires READY and current owner fence")
async def initialize(graph, key, messages, digest, store, attempt, owner, fence):
config = {"configurable": {"thread_id": key}}
marker = {"history_attempt": attempt, "history_digest": digest}
async with store.locked(key):
current = await graph.aget_state(config)
if store.get(key) is None and current.created_at is not None:
raise ValueError("unowned history checkpoint already exists")
store.claim(key, attempt, digest, owner, fence)
if current.created_at is None:
await graph.aupdate_state(dict(config, metadata=dict(marker, history_stage="START")),
{"messages": list(messages)}, as_node=START)
current = await graph.aget_state(config)
metadata = current.metadata or {}
if any(metadata.get(k) != v for k, v in marker.items()):
raise ValueError("unowned history checkpoint")
if current.values.get("messages", []) != list(messages):
raise ValueError("history checkpoint content changed")
stage = metadata.get("history_stage")
if stage == "START":
await graph.aupdate_state(dict(config, metadata=dict(marker, history_stage="END")),
None, as_node=END)
current = await graph.aget_state(config)
elif stage != "END":
raise ValueError("unknown history initialization stage")
if current.next or current.tasks:
raise ValueError("history checkpoint is not READY")
store.status(key, attempt, owner, fence, store.get(key)["status"], "READY")
return config
async def invoke(graph, key, store, attempt, owner, fence, input):
async with store.locked(key):
# Consume before execution: uncertain invocation must not be replayed.
store.status(key, attempt, owner, fence, "READY", "CONSUMED")
return await graph.ainvoke(input, {"configurable": {"thread_id": key}})
+244
View File
@@ -0,0 +1,244 @@
"""Committed history to a fresh turn checkpoint, never an old stack resume.
The host supplies authorized, revision-consistent records and owns the turn
fence across creation and invocation. This module does not read or modify PG.
"""
from __future__ import annotations
import copy
import hashlib
import json
from dataclasses import dataclass
from typing import Any, Sequence
from langchain_core.messages import BaseMessage, HumanMessage, ToolMessage
from langchain_core.runnables import RunnableConfig
from langgraph.graph import END, START
from .patches import _sanitize_openai_tool_history, _validate_openai_tool_history
from .history_initialization import SqliteInitializationStore, initialize, invoke
@dataclass(frozen=True)
class HistoryScope:
tenant_id: str
thread_id: str
turn_id: str
workspace_id: str
graph_version: str
tool_version: str
history_revision: int
def validate(self) -> None:
for value in (self.tenant_id, self.thread_id, self.turn_id,
self.workspace_id, self.graph_version, self.tool_version):
if not isinstance(value, str) or not value.strip():
raise ValueError("history scope fields must be nonempty strings")
if type(self.history_revision) is not int or self.history_revision < 0:
raise ValueError("invalid history revision")
@dataclass(frozen=True)
class HistoryRecord:
message_id: str
revision: int
message: BaseMessage
partial: bool = False
file_refs: tuple[str, ...] = ()
@dataclass(frozen=True)
class NormalizedHistory:
scope: HistoryScope
records: tuple[HistoryRecord, ...]
messages: tuple[BaseMessage, ...]
def _check_scope(source: HistoryScope, expected: HistoryScope) -> None:
source.validate()
expected.validate()
if source != expected:
raise ValueError("history scope/version/revision mismatch")
def _text(content: Any) -> str:
# Only text is replayed. Media bytes stay outside model history; authorized
# file references are supplied separately by the host's record adapter.
if isinstance(content, str):
return "[inline media omitted]" if "base64," in content or "data:" in content else content
if isinstance(content, list):
return "\n".join(_text(b.get("text", "")) for b in content
if isinstance(b, dict) and b.get("type") == "text")
return ""
def normalize_history(records: Sequence[HistoryRecord], *, source: HistoryScope,
expected: HistoryScope) -> NormalizedHistory:
"""Pure normalization of already-authorized stable-ID committed records.
Complete parallel tool pairs survive; unmatched calls become sourced facts,
never invented ToolMessages. Raw messages and provider metadata are untouched.
"""
_check_scope(source, expected)
seen: set[str] = set()
messages = []
notes = []
for record in records:
if (not isinstance(record.message_id, str) or not record.message_id.strip()
or record.message_id in seen or type(record.revision) is not int
or record.revision < 0 or not isinstance(record.message, BaseMessage)):
raise ValueError("invalid or duplicate history record")
seen.add(record.message_id)
message = copy.deepcopy(record.message)
message.id = record.message_id
messages.append(message)
calls = {}
results = set()
for message in messages:
for call in getattr(message, "tool_calls", []):
call_id = call.get("id")
if not call_id or call_id in calls:
raise ValueError("missing or duplicate tool call ID")
calls[call_id] = call.get("name")
if isinstance(message, ToolMessage):
call_id = message.tool_call_id
if not call_id or call_id not in calls or call_id in results:
raise ValueError("unassociated or duplicate tool result")
if message.name is not None and message.name != calls[call_id]:
raise ValueError("tool result name conflicts with call ID")
results.add(call_id)
repaired = _sanitize_openai_tool_history(messages)
retained = {m.id: m for m in repaired}
for record, original in zip(records, messages):
repaired_message = retained.get(record.message_id)
kept = {c.get("id") for c in getattr(repaired_message, "tool_calls", [])}
missing = [c.get("id") or "unidentified" for c in getattr(original, "tool_calls", [])
if c.get("id") not in kept]
provenance = f"source={record.message_id}@{record.revision}"
if missing or getattr(original, "invalid_tool_calls", []):
notes.append(f"[{provenance}] incomplete tool calls (result unavailable; execution unknown): {missing}")
if record.partial:
notes.append(f"[{provenance}] partial result, not a completed answer")
for ref in record.file_refs:
if not isinstance(ref, str) or not ref.strip() or "data:" in ref or "base64" in ref:
raise ValueError("invalid file reference")
notes.append(f"[{provenance}] file reference: {ref}")
for message in repaired:
message.content = _text(message.content)
message.additional_kwargs = {}
message.response_metadata = {}
if hasattr(message, "artifact"):
message.artifact = None
# Tool arguments are historical context, not executable input. Reject
# embedded binary payload rather than stringify it into provider text.
if "base64" in json.dumps(getattr(message, "tool_calls", [])):
raise ValueError("inline binary tool arguments are not replayable")
if notes:
repaired.append(HumanMessage(content="Historical context notes:\n" + "\n".join(notes),
id="history-notes:" + str(source.history_revision)))
_validate_openai_tool_history(repaired)
return NormalizedHistory(source, tuple(copy.deepcopy(records)), tuple(repaired))
def committed_history_input(history: dict, current: dict, *, thread_id: str,
run_id: str, checkpoint_exists: bool = False) -> dict:
"""Project PG display records into the existing worker's message channel.
Display tool items are sourced facts, not provider tool protocol. The
reducer sentinel is only used when the host attests no compatible checkpoint
exists. Later turns append without resetting the summarizer's cut indexes.
No second invocation or checkpoint mutation is performed by the HTTP host.
"""
from langchain_core.messages import AIMessage, RemoveMessage, convert_to_messages
from langgraph.graph.message import REMOVE_ALL_MESSAGES
if (history.get("schema") != "ai4sci.committed-history.v1"
or history.get("thread_id") != thread_id
or not isinstance(history.get("records"), list)
or not isinstance(current, dict)
or not isinstance(current.get("messages"), list)):
raise ValueError("invalid committed history input")
scope = HistoryScope(
tenant_id=history["user_uid"], thread_id=thread_id, turn_id=run_id,
workspace_id=thread_id, graph_version="worker-input-v1",
tool_version="display-facts-v2", history_revision=history["conversation_revision"],
)
records = []
for row in sorted(history["records"], key=lambda r: (r["message_index"], r["message_id"])):
if row["message_id"] == history.get("excluded_message_id"):
continue
payload = row["payload"]
role = row["role"]
if role not in {"user", "assistant"}:
raise ValueError("unsupported committed history role")
text = []
refs = []
if role == "user":
text.append(_text(payload.get("content", "")))
for attachment in payload.get("attached_files") or []:
if isinstance(attachment, dict):
ref = attachment.get("virtual_path") or attachment.get("path")
if isinstance(ref, str) and ref.startswith("/workspace/"):
refs.append(ref)
else:
for item in sorted(payload.get("items") or [], key=lambda i: i["item_sequence"]):
if item.get("status") == "superseded":
continue
kind = item.get("type")
source = f"[{row['message_id']}@{row['revision']}:{item['item_id']}]"
if kind == "message":
text.extend(part["text"] for part in item.get("content", [])
if part.get("type") in {"output_text", "refusal"})
elif kind in {"tool_call", "tool_output", "agent_status", "summarization"}:
fields = {key: item[key] for key in
("name", "input", "output", "result", "summary", "status") if key in item}
text.append(f"{source} historical {kind}: " + json.dumps(fields, ensure_ascii=False))
elif kind == "artifact" and str(item.get("virtual_path", "")).startswith("/workspace/"):
refs.append(item["virtual_path"])
message = (HumanMessage if role == "user" else AIMessage)(content="\n".join(text))
records.append(HistoryRecord(row["message_id"], row["revision"], message,
partial=bool(payload.get("incomplete")), file_refs=tuple(refs)))
normalized = normalize_history(records, source=scope, expected=scope)
incoming = convert_to_messages(copy.deepcopy(current["messages"]))
for index, message in enumerate(incoming):
# Stable IDs ensure replay cannot duplicate the current turn either.
message.id = f"current:{run_id}:{index}"
if checkpoint_exists:
return {**current, "messages": [m.model_dump(mode="json") for m in incoming]}
messages = [RemoveMessage(id=REMOVE_ALL_MESSAGES), *normalized.messages, *incoming]
# Old summary cut indexes refer to the replaced checkpoint message list.
# The existing summarizer/budget middleware recomputes them for this input.
return {**current, "messages": [m.model_dump(mode="json") for m in messages],
"_summarization_event": None}
def history_key(scope: HistoryScope) -> str:
scope.validate()
identity = json.dumps(list(vars(scope).values()), separators=(",", ":"))
return "history-v1:" + hashlib.sha256(identity.encode()).hexdigest()
async def invoke_history_checkpoint(graph, *, scope, store, attempt, owner, fence, input):
return await invoke(graph, history_key(scope), store, attempt, owner, fence, input)
async def create_history_checkpoint(graph: Any, history: NormalizedHistory, *,
expected: HistoryScope, store=None, attempt=None,
owner=None, fence=None) -> RunnableConfig:
"""Seed through a durable gate; dispatch only via invoke_history_checkpoint.
A matching attempt/content may recover initialization, never execution.
The host owns authorization and issues monotonically increasing fences.
"""
_check_scope(history.scope, expected)
normalized = normalize_history(history.records, source=history.scope, expected=expected)
key = history_key(expected)
if store is not None:
payload = [{"id": r.message_id, "revision": r.revision,
"message": r.message.model_dump(mode="json"),
"partial": r.partial, "files": r.file_refs} for r in history.records]
digest = hashlib.sha256(json.dumps(payload, sort_keys=True,
separators=(",", ":")).encode()).hexdigest()
return await initialize(graph, key, normalized.messages, digest,
store, attempt, owner, fence)
raise ValueError("durable initialization store is required")
+370
View File
@@ -0,0 +1,370 @@
"""Opt-in host identity prototype. Records never prove resource quiescence.
Trusted local host API, not an authenticated remote control endpoint. A future
PG host store can implement this protocol without introducing a dispatcher.
Only the runtime resource owner may attest cleanup and release its claim.
"""
from contextlib import closing, contextmanager
from pathlib import Path
import sqlite3
import json
import hashlib
import os
from typing import Protocol
from .contracts import EvoRuntimeError, canonical_json_v1
class HostExecutionRegistry(Protocol):
def claim_checkpoint(self, execution_id: str, *, store_id: str, checkpoint_thread_id: str, subject_id: str) -> None: ...
def release_unbound_checkpoint(self, execution_id: str) -> None: ...
def continuation(self, execution_id: str) -> dict: ...
def prepare_terminal(self, execution_id: str, *, event: dict) -> None: ...
def terminal_intent(self, execution_id: str) -> dict | None: ...
def confirm_terminal(self, execution_id: str, *, digest: str) -> None: ...
def finish(self, execution_id: str, *, outcome: str, checkpoint_id: str = "") -> None: ...
def bind(self, *, execution_id: str, grant_id: str, digest: str,
thread_id: str, turn_id: str, predecessor_execution_id: str = "",
predecessor_checkpoint_id: str = "", predecessor_owner_epoch: int = 0,
continuation_pending_hash: str = "", continuation_decision_hash: str = "") -> None: ...
def lookup_grant(self, grant_id: str, digest: str) -> dict | None: ...
def inspect(self, execution_id: str) -> dict: ...
def transfer_control(self, execution_id: str, *, expected_epoch: int, new_epoch: int) -> int: ...
def require_control(self, execution_id: str, *, owner_epoch: int) -> None: ...
def inspect_control(self, execution_id: str, *, owner_epoch: int | None, boot_id: str | None) -> dict: ...
def accept_cancel(self, execution_id: str, *, owner_epoch: int | None, boot_id: str | None, reason: str) -> int: ...
class SQLiteHostRegistry:
def __init__(self, path: str | Path, *, host_id: str, boot_id: str):
self.path, self.host_id, self.boot_id = str(path), host_id, boot_id
directory = Path(path).parent
directory.mkdir(mode=0o700, parents=True, exist_ok=True)
# Exclusive creation gives new user-content stores safe defaults without
# changing permissions on an existing host's directory or database.
try:
fd = os.open(self.path, os.O_CREAT | os.O_EXCL | os.O_WRONLY, 0o600)
except FileExistsError:
pass
else:
os.close(fd)
with self._transaction() as db:
db.execute("BEGIN IMMEDIATE")
db.execute("""CREATE TABLE IF NOT EXISTS executions (
execution_id TEXT PRIMARY KEY, grant_id TEXT UNIQUE NOT NULL,
digest TEXT NOT NULL, thread_id TEXT NOT NULL,
turn_id TEXT NOT NULL, host_id TEXT NOT NULL, boot_id TEXT NOT NULL,
owner_epoch INTEGER NOT NULL DEFAULT 1)""")
schema = db.execute("SELECT sql FROM sqlite_master WHERE name='executions'").fetchone()[0]
if "UNIQUE(thread_id, turn_id)" in schema:
db.execute("ALTER TABLE executions RENAME TO legacy_executions")
db.execute("""CREATE TABLE executions (
execution_id TEXT PRIMARY KEY, grant_id TEXT UNIQUE NOT NULL,
digest TEXT NOT NULL, thread_id TEXT NOT NULL, turn_id TEXT NOT NULL,
host_id TEXT NOT NULL, boot_id TEXT NOT NULL,
owner_epoch INTEGER NOT NULL DEFAULT 1)""")
db.execute("INSERT INTO executions SELECT * FROM legacy_executions")
db.execute("DROP TABLE legacy_executions")
db.execute("""CREATE TABLE IF NOT EXISTS cancel_intents (
intent_id INTEGER PRIMARY KEY AUTOINCREMENT,
execution_id TEXT NOT NULL, owner_epoch INTEGER NOT NULL,
boot_id TEXT NOT NULL, reason TEXT NOT NULL)""")
db.execute("""CREATE TABLE IF NOT EXISTS terminal_evidence (
execution_id TEXT PRIMARY KEY, outcome TEXT NOT NULL)""")
db.execute("""CREATE TABLE IF NOT EXISTS terminal_intents (
execution_id TEXT PRIMARY KEY, event_json TEXT NOT NULL,
digest TEXT NOT NULL, phase TEXT NOT NULL,
cleanup_boot_id TEXT NOT NULL)""")
db.execute("""CREATE TABLE IF NOT EXISTS pending_continuations (
execution_id TEXT PRIMARY KEY, checkpoint_id TEXT NOT NULL,
consumed_by TEXT UNIQUE)""")
columns = {row[1] for row in db.execute("PRAGMA table_info(pending_continuations)")}
for name in ("pending_hash", "decision_hash"):
if name not in columns:
db.execute(f"ALTER TABLE pending_continuations ADD COLUMN {name} TEXT NOT NULL DEFAULT ''")
db.execute("""CREATE TABLE IF NOT EXISTS active_claims (
execution_id TEXT PRIMARY KEY, thread_id TEXT NOT NULL,
turn_id TEXT NOT NULL, UNIQUE(thread_id, turn_id))""")
db.execute("""INSERT OR IGNORE INTO active_claims
SELECT execution_id, thread_id, turn_id FROM executions
WHERE execution_id NOT IN (SELECT execution_id FROM terminal_evidence)""")
db.execute("""CREATE TABLE IF NOT EXISTS checkpoint_scopes (
store_id TEXT NOT NULL, checkpoint_thread_id TEXT NOT NULL,
checkpoint_ns TEXT NOT NULL, subject_id TEXT NOT NULL,
PRIMARY KEY(store_id, checkpoint_thread_id, checkpoint_ns))""")
db.execute("""CREATE TABLE IF NOT EXISTS checkpoint_writers (
execution_id TEXT PRIMARY KEY, store_id TEXT NOT NULL,
checkpoint_thread_id TEXT NOT NULL, checkpoint_ns TEXT NOT NULL,
host_id TEXT NOT NULL, boot_id TEXT NOT NULL,
UNIQUE(store_id, checkpoint_thread_id, checkpoint_ns))""")
db.execute("""CREATE TABLE IF NOT EXISTS execution_checkpoint_scopes (
execution_id TEXT PRIMARY KEY, store_id TEXT NOT NULL,
checkpoint_thread_id TEXT NOT NULL, checkpoint_ns TEXT NOT NULL)""")
def claim_checkpoint(self, execution_id: str, *, store_id: str,
checkpoint_thread_id: str, subject_id: str) -> None:
"""Root Graph writer, including child namespaces; no expiry/takeover."""
scope = (store_id, checkpoint_thread_id, "")
if not all((execution_id, store_id, checkpoint_thread_id, subject_id)):
raise EvoRuntimeError("CHECKPOINT_SCOPE_INVALID")
with self._transaction() as db:
db.execute("BEGIN IMMEDIATE")
owner = db.execute("SELECT subject_id FROM checkpoint_scopes WHERE store_id=? AND checkpoint_thread_id=? AND checkpoint_ns=?", scope).fetchone()
if owner and owner[0] != subject_id:
raise EvoRuntimeError("CHECKPOINT_SUBJECT_MISMATCH")
if db.execute("SELECT 1 FROM checkpoint_writers WHERE store_id=? AND checkpoint_thread_id=? AND checkpoint_ns=?", scope).fetchone():
raise EvoRuntimeError("CHECKPOINT_WRITER_BUSY")
if db.execute("SELECT 1 FROM active_claims WHERE execution_id NOT IN (SELECT execution_id FROM execution_checkpoint_scopes)").fetchone():
raise EvoRuntimeError("CHECKPOINT_LEGACY_WRITER_UNKNOWN")
db.execute("INSERT OR IGNORE INTO checkpoint_scopes VALUES (?, ?, ?, ?)", (*scope, subject_id))
db.execute("INSERT INTO checkpoint_writers VALUES (?, ?, ?, ?, ?, ?)",
(execution_id, *scope, self.host_id, self.boot_id))
def release_unbound_checkpoint(self, execution_id: str) -> None:
"""Only preparation failure, before bind/construction/writes began."""
with self._transaction() as db:
db.execute("BEGIN IMMEDIATE")
if db.execute("SELECT 1 FROM executions WHERE execution_id=?", (execution_id,)).fetchone():
raise EvoRuntimeError("CHECKPOINT_WRITER_ALREADY_BOUND")
db.execute("DELETE FROM checkpoint_writers WHERE execution_id=? AND host_id=? AND boot_id=?",
(execution_id, self.host_id, self.boot_id))
def _connect(self):
db = sqlite3.connect(self.path, timeout=2)
db.row_factory = sqlite3.Row
db.execute("PRAGMA synchronous=FULL")
return db
@contextmanager
def _transaction(self):
try:
with closing(self._connect()) as db, db:
yield db
except sqlite3.OperationalError as exc:
code = getattr(exc, "sqlite_errorcode", 0) & 255
if code in {sqlite3.SQLITE_BUSY, sqlite3.SQLITE_LOCKED}:
raise EvoRuntimeError("HOST_REGISTRY_BUSY") from exc
raise
def bind(self, *, execution_id: str, grant_id: str, digest: str,
thread_id: str, turn_id: str, predecessor_execution_id: str = "",
predecessor_checkpoint_id: str = "", predecessor_owner_epoch: int = 0,
continuation_pending_hash: str = "", continuation_decision_hash: str = "") -> None:
with self._transaction() as db:
db.execute("BEGIN IMMEDIATE")
if db.execute("SELECT 1 FROM executions WHERE execution_id=? OR grant_id=?",
(execution_id, grant_id)).fetchone():
raise EvoRuntimeError("EXECUTION_IDENTITY_CONFLICT")
writer = db.execute("SELECT * FROM checkpoint_writers WHERE execution_id=?", (execution_id,)).fetchone()
if writer and (writer["host_id"] != self.host_id or writer["boot_id"] != self.boot_id):
raise EvoRuntimeError("EXECUTION_BOOT_MISMATCH")
if writer and predecessor_execution_id:
previous = db.execute("SELECT * FROM execution_checkpoint_scopes WHERE execution_id=?", (predecessor_execution_id,)).fetchone()
if previous is None or any(previous[k] != writer[k] for k in ("store_id", "checkpoint_thread_id", "checkpoint_ns")):
raise EvoRuntimeError("CONTINUATION_CHECKPOINT_SCOPE_MISMATCH")
if db.execute("SELECT 1 FROM active_claims WHERE thread_id=? AND turn_id=?",
(thread_id, turn_id)).fetchone():
raise EvoRuntimeError("TURN_EXECUTION_UNKNOWN")
prior = db.execute("SELECT 1 FROM executions WHERE thread_id=? AND turn_id=?",
(thread_id, turn_id)).fetchone()
if prior or predecessor_execution_id or predecessor_checkpoint_id:
if not predecessor_execution_id:
raise EvoRuntimeError("CONTINUATION_REQUIRED")
pending = db.execute("""SELECT p.*, e.owner_epoch, e.host_id FROM pending_continuations p
JOIN executions e USING(execution_id)
WHERE p.execution_id=? AND e.thread_id=? AND e.turn_id=?""",
(predecessor_execution_id, thread_id, turn_id)).fetchone()
if (pending is not None and pending["consumed_by"] is not None
and pending["checkpoint_id"] == predecessor_checkpoint_id):
failed = db.execute(
"SELECT 1 FROM terminal_evidence WHERE execution_id=? AND outcome='failed'",
(pending["consumed_by"],),
).fetchone()
if failed:
# Never unconsume a decision on failure. A new grant alone
# is insufficient; recovery needs fresh pending authority.
raise EvoRuntimeError("CONTINUATION_CONSUMED_FAILURE_REQUIRES_REAUTHORIZATION")
if (pending is None or not predecessor_checkpoint_id
or pending["checkpoint_id"] != predecessor_checkpoint_id
or pending["consumed_by"] is not None):
raise EvoRuntimeError("CONTINUATION_INVALID")
if (pending["host_id"] != self.host_id or not pending["pending_hash"]
or pending["owner_epoch"] != predecessor_owner_epoch
or isinstance(predecessor_owner_epoch, bool)
or pending["pending_hash"] != continuation_pending_hash
or len(continuation_decision_hash) != 64):
raise EvoRuntimeError("CONTINUATION_AUTHORIZATION_INVALID")
if db.execute("SELECT 1 FROM cancel_intents WHERE execution_id=?",
(predecessor_execution_id,)).fetchone():
raise EvoRuntimeError("CONTINUATION_CANCELLED")
db.execute("UPDATE pending_continuations SET consumed_by=?, decision_hash=? WHERE execution_id=?",
(execution_id, continuation_decision_hash, predecessor_execution_id))
db.execute("INSERT INTO executions VALUES (?, ?, ?, ?, ?, ?, ?, 1)",
(execution_id, grant_id, digest, thread_id, turn_id,
self.host_id, self.boot_id))
db.execute("INSERT INTO active_claims VALUES (?, ?, ?)",
(execution_id, thread_id, turn_id))
if writer:
db.execute("INSERT INTO execution_checkpoint_scopes VALUES (?, ?, ?, ?)",
(execution_id, writer["store_id"], writer["checkpoint_thread_id"], writer["checkpoint_ns"]))
def finish(self, execution_id: str, *, outcome: str, checkpoint_id: str = "") -> None:
"""Trusted resource-owner attestation, not transferable control authority."""
self._finish(execution_id, outcome=outcome, checkpoint_id=checkpoint_id)
def prepare_terminal(self, execution_id: str, *, event: dict) -> None:
"""Original resource owner only, AFTER all owned cleanup returns."""
body = canonical_json_v1(event).decode()
digest = hashlib.sha256(body.encode()).hexdigest()
if (event.get("run_id") != execution_id or event.get("kind") != "run"
or event.get("payload", {}).get("kind") != "run_terminal"):
raise EvoRuntimeError("EXECUTION_TERMINAL_CONFLICT")
with self._transaction() as db:
db.execute("BEGIN IMMEDIATE")
row = db.execute("SELECT * FROM executions WHERE execution_id=?", (execution_id,)).fetchone()
if row is None or row['host_id'] != self.host_id or row['boot_id'] != self.boot_id:
raise EvoRuntimeError("EXECUTION_BOOT_MISMATCH")
prior = db.execute("SELECT digest FROM terminal_intents WHERE execution_id=?", (execution_id,)).fetchone()
if prior and prior['digest'] != digest:
raise EvoRuntimeError("EXECUTION_TERMINAL_CONFLICT")
db.execute("INSERT OR IGNORE INTO terminal_intents VALUES (?, ?, ?, 'prepared', ?)",
(execution_id, body, digest, self.boot_id))
def terminal_intent(self, execution_id: str) -> dict | None:
with closing(self._connect()) as db:
row = db.execute("""SELECT i.* FROM terminal_intents i JOIN executions e USING(execution_id)
WHERE execution_id=? AND e.host_id=?""", (execution_id, self.host_id)).fetchone()
if row is None:
return None
return {**dict(row), 'event': json.loads(row['event_json']), 'cleanup_confirmed': True}
def confirm_terminal(self, execution_id: str, *, digest: str) -> None:
with self._transaction() as db:
row = db.execute("""SELECT i.* FROM terminal_intents i JOIN executions e USING(execution_id)
WHERE execution_id=? AND e.host_id=?""", (execution_id, self.host_id)).fetchone()
if row is None or row['digest'] != digest:
raise EvoRuntimeError("EXECUTION_TERMINAL_CONFLICT")
db.execute("UPDATE terminal_intents SET phase='sink_confirmed' WHERE execution_id=? AND phase='prepared'",
(execution_id,))
def _finish(self, execution_id: str, *, outcome: str, checkpoint_id: str) -> None:
if outcome not in {"completed", "cancelled", "failed", "awaiting_input"}:
raise EvoRuntimeError("EXECUTION_OUTCOME_INVALID")
with self._transaction() as db:
db.execute("BEGIN IMMEDIATE")
row = db.execute("SELECT * FROM executions WHERE execution_id=?",
(execution_id,)).fetchone()
intent = db.execute("SELECT * FROM terminal_intents WHERE execution_id=?", (execution_id,)).fetchone()
if row is None or row["host_id"] != self.host_id or (row["boot_id"] != self.boot_id and
(intent is None or intent['phase'] not in {'sink_confirmed', 'registry_finished'})):
raise EvoRuntimeError("EXECUTION_BOOT_MISMATCH")
if intent is not None:
payload = json.loads(intent['event_json'])['payload']
if (intent['phase'] not in {'sink_confirmed', 'registry_finished'}
or payload['outcome'] != outcome
or str(payload.get('checkpoint_id') or '') != checkpoint_id):
raise EvoRuntimeError("EXECUTION_TERMINAL_CONFLICT")
prior = db.execute("SELECT outcome FROM terminal_evidence WHERE execution_id=?",
(execution_id,)).fetchone()
if prior is not None and prior["outcome"] != outcome:
raise EvoRuntimeError("EXECUTION_TERMINAL_CONFLICT")
db.execute("INSERT OR IGNORE INTO terminal_evidence VALUES (?, ?)",
(execution_id, outcome))
if outcome == "awaiting_input" and checkpoint_id:
pending = db.execute("SELECT checkpoint_id FROM pending_continuations WHERE execution_id=?",
(execution_id,)).fetchone()
if pending is not None and pending["checkpoint_id"] != checkpoint_id:
raise EvoRuntimeError("EXECUTION_TERMINAL_CONFLICT")
identity = {}
if intent is not None:
payload = json.loads(intent['event_json'])['payload']
identity = {k: payload.get(k) for k in (
"checkpoint_thread_id", "checkpoint_id", "checkpoint_ns", "pending_interrupts")}
pending_hash = hashlib.sha256(canonical_json_v1(identity)).hexdigest() if identity else ""
db.execute("INSERT OR IGNORE INTO pending_continuations "
"(execution_id, checkpoint_id, consumed_by, pending_hash, decision_hash) VALUES (?, ?, NULL, ?, '')",
(execution_id, checkpoint_id, pending_hash))
db.execute("DELETE FROM active_claims WHERE execution_id=?", (execution_id,))
db.execute("DELETE FROM checkpoint_writers WHERE execution_id=?", (execution_id,))
db.execute("UPDATE terminal_intents SET phase='registry_finished' WHERE execution_id=?", (execution_id,))
def continuation(self, execution_id: str) -> dict:
with closing(self._connect()) as db:
row = db.execute("""SELECT p.*, e.owner_epoch FROM pending_continuations p
JOIN executions e USING(execution_id) WHERE execution_id=? AND e.host_id=?""",
(execution_id, self.host_id)).fetchone()
if row is None:
raise EvoRuntimeError("CONTINUATION_INVALID")
return dict(row)
def lookup_grant(self, grant_id: str, digest: str) -> dict | None:
with closing(self._connect()) as db:
row = db.execute("SELECT * FROM executions WHERE grant_id=?", (grant_id,)).fetchone()
if row is None:
return None
if row["digest"] != digest:
raise EvoRuntimeError("CONTRACT_REPLAYED")
return dict(row)
def transfer_control(self, execution_id: str, *, expected_epoch: int, new_epoch: int) -> int:
if new_epoch <= expected_epoch:
raise EvoRuntimeError("OWNER_EPOCH_STALE")
with self._transaction() as db:
changed = db.execute(
"UPDATE executions SET owner_epoch=? WHERE execution_id=? AND owner_epoch=? AND host_id=? AND boot_id=?",
(new_epoch, execution_id, expected_epoch, self.host_id, self.boot_id),
).rowcount
if changed != 1:
raise EvoRuntimeError("OWNER_EPOCH_STALE")
return new_epoch
def require_control(self, execution_id: str, *, owner_epoch: int) -> None:
with closing(self._connect()) as db:
row = db.execute("SELECT owner_epoch FROM executions WHERE execution_id=?",
(execution_id,)).fetchone()
if row is None or row["owner_epoch"] != owner_epoch:
raise EvoRuntimeError("OWNER_EPOCH_STALE")
def inspect(self, execution_id: str) -> dict:
with closing(self._connect()) as db:
row = db.execute("SELECT * FROM executions WHERE execution_id=?", (execution_id,)).fetchone()
terminal = db.execute("SELECT outcome FROM terminal_evidence WHERE execution_id=?",
(execution_id,)).fetchone()
cancel = db.execute("SELECT 1 FROM cancel_intents WHERE execution_id=? LIMIT 1",
(execution_id,)).fetchone()
return {**(dict(row) if row else {"execution_id": execution_id}),
"cancel_requested": cancel is not None,
"recovery_action": "inspect_only",
"status": terminal["outcome"] if terminal else "unknown",
"resources_confirmed_exited": terminal is not None,
"source": "host_binding_only" if row else "no_host_binding"}
def _control_row(self, db, execution_id, owner_epoch, boot_id):
if owner_epoch is None:
raise EvoRuntimeError("OWNER_EPOCH_REQUIRED")
row = db.execute("SELECT * FROM executions WHERE execution_id=?", (execution_id,)).fetchone()
if row is None:
raise EvoRuntimeError("EXECUTION_UNKNOWN")
if row["host_id"] != self.host_id or row["boot_id"] != self.boot_id or boot_id != self.boot_id:
raise EvoRuntimeError("EXECUTION_BOOT_MISMATCH")
if isinstance(owner_epoch, bool) or row["owner_epoch"] != owner_epoch:
raise EvoRuntimeError("OWNER_EPOCH_STALE")
return row
def inspect_control(self, execution_id: str, *, owner_epoch: int | None, boot_id: str | None) -> dict:
with closing(self._connect()) as db:
row = self._control_row(db, execution_id, owner_epoch, boot_id)
return self.inspect(execution_id)
def accept_cancel(self, execution_id: str, *, owner_epoch: int | None, boot_id: str | None, reason: str) -> int:
# COMMIT is the linearization point shared with transfer's conditional UPDATE.
# Accepted commands survive a later transfer; no write lock crosses an await.
with self._transaction() as db:
db.execute("BEGIN IMMEDIATE")
self._control_row(db, execution_id, owner_epoch, boot_id)
cursor = db.execute(
"INSERT INTO cancel_intents (execution_id, owner_epoch, boot_id, reason) VALUES (?, ?, ?, ?)",
(execution_id, owner_epoch, boot_id, reason),
)
assert cursor.lastrowid is not None
return cursor.lastrowid
+2 -1
View File
@@ -83,8 +83,9 @@ def compile_invocation_plan(
"""Validate and freeze adapter output before constructing a provider SDK."""
params = dict(sdk_params)
streaming = purpose == "main_agent"
streaming = True
params["streaming"] = streaming
params["disable_streaming"] = False
if runtime_provider == "openai" and streaming:
params["stream_usage"] = True
token_fields = tuple(
+47 -5
View File
@@ -14,7 +14,7 @@ import stat
import tempfile
import time
from collections.abc import Mapping, Sequence
from dataclasses import asdict, dataclass, field
from dataclasses import asdict, dataclass, field, replace
from decimal import Decimal, InvalidOperation
from pathlib import Path
from typing import Any
@@ -477,12 +477,49 @@ class EvoModelConfig:
pools = _parse_endpoint_pools(raw.get("endpoint_pools"), providers)
health = _parse_health(raw.get("route_health"))
selectors = _parse_selectors(raw.get("route_selectors"), providers, pools)
# Schema2's historical non_streaming Web tool routes and native routes
# are administrator declarations, not probes or HTTP streaming policy.
declared_tool_models = {
(selector.provider, selector.model)
for selector in selectors.values()
if selector.tool_call_transport in {"native", "non_streaming"}
}
providers = {
provider_id: replace(
provider,
models={
model_id: replace(
model,
capabilities={
**model.capabilities,
"tools": (provider_id, model_id) in declared_tool_models,
},
)
for model_id, model in provider.models.items()
},
)
for provider_id, provider in providers.items()
}
main_routes, title_selector_id = _parse_purpose_routes(
raw.get("purpose_routes"), selectors
)
limits = _parse_call_limits(raw.get("purpose_call_limits"))
web_runtime = _parse_web_runtime(raw.get("web_runtime"))
fallbacks = _parse_fallbacks(raw.get("tool_protocol_fallbacks"), selectors)
execution_selectors = set(main_routes.selectable.values()) | {
title_selector_id
}
for primary, candidates in fallbacks.items():
execution_selectors.add(primary)
execution_selectors.update(candidates)
for selector_id in sorted(execution_selectors):
selector = selectors[selector_id]
if (selector.provider, selector.model) not in declared_tool_models:
raise EvoRuntimeError(
"LLM_ROUTE_CONFIGURATION_REQUIRED",
"schema2 tool capability migration requires an explicit "
f"native or historical non_streaming declaration for {selector_id}",
)
config = cls(
config_revision=revision,
config_identity_key_id=identity_key_id,
@@ -1973,6 +2010,7 @@ def _parse_providers(value: Any) -> Mapping[str, ProviderConfig]:
_string_set(access.get("allowed_plans"), "allowed_plans"),
_string_set(access.get("allowed_roles"), "allowed_roles"),
quote,
capabilities={"tools": False, "text": True},
)
result[key] = ProviderConfig(
key,
@@ -2137,10 +2175,12 @@ def _parse_selectors(
transport = _text(
item.get("tool_call_transport"), "selector.tool_call_transport"
)
if transport != "non_streaming":
# Legacy values remain readable for signed route evidence, but do not
# control HTTP streaming. Tool support and HTTP mode are independent.
if transport not in {"native", "streaming", "non_streaming"}:
raise EvoRuntimeError(
"LLM_ROUTE_CONFIGURATION_REQUIRED",
"Web tool routes must be non_streaming",
"Web tool routes require native tool support",
)
_validate_params(
provider.params, name=f"providers.{provider_id}.params", allowed=allowed
@@ -3180,14 +3220,16 @@ def convert_v2_to_v3_draft(
"api_mode": selector.api_mode
if selector
else "chat_completions",
"tool_call_transport": "native",
"tool_call_transport": "native"
if model.capabilities.get("tools", False)
else "disabled",
},
"capabilities": {
"text": True,
"vision": model.supports_vision,
"video": False,
"documents": False,
"tools": True,
"tools": bool(model.capabilities.get("tools", False)),
"structured_output": False,
"thinking": model.supports_reasoning,
},
+1 -1
View File
@@ -572,7 +572,7 @@ def get_chat_model(
_original_provider = provider
base_url_default, api_key_env = _OPENAI_ROUTED_PROVIDERS[provider]
if provider == "custom-openai":
base_url = os.environ.get("CUSTOM_OPENAI_BASE_URL", "")
base_url = str(kwargs.get("base_url") or os.environ.get("CUSTOM_OPENAI_BASE_URL", ""))
if not base_url:
raise ValueError(
"CUSTOM_OPENAI_BASE_URL environment variable is required when using "
+660 -31
View File
@@ -15,6 +15,7 @@ import traceback
import uuid
from collections import defaultdict
from collections.abc import AsyncIterator, Callable, Mapping, Sequence
from contextvars import ContextVar
from dataclasses import asdict, dataclass, field, replace
from typing import Any
@@ -35,6 +36,7 @@ from .contracts import (
ModelAttemptEvent,
ModelCatalog,
ModelCatalogEntry,
ModelFactoryResult,
PreparedRunQuote,
PricingQuote,
RouteCallBound,
@@ -46,6 +48,7 @@ from .contracts import (
now_ms,
)
from .crypto import HmacKeyRing, canonical_json_v1
from .host_execution_registry import HostExecutionRegistry
from .invocation import InvocationPlan, compile_invocation_plan
from .model_config import (
EvoModelConfig,
@@ -65,6 +68,7 @@ from .user_options import (
_BIGINT_MAX = 2**63 - 1
logger = logging.getLogger(__name__)
_construction_owner: ContextVar[Any] = ContextVar("evo_construction_owner", default=None)
_ROUTE_SEMANTICS_INFO = "ai4sci/route-semantics-hash/v3"
_ROUTE_FINGERPRINT_INFO = "ai4sci/route-fingerprint/v3"
_ENDPOINT_FINGERPRINT_INFO = "ai4sci/endpoint-fingerprint/v3"
@@ -382,6 +386,7 @@ class EvoModelRuntime:
]
| None = None,
runtime_instance_id: str | None = None,
host_registry: HostExecutionRegistry | None = None,
) -> None:
self.store = store
self.admission_verifier = admission_verifier
@@ -399,6 +404,7 @@ class EvoModelRuntime:
self.model_factory = model_factory or self._default_model_factory
self.agent_factory = agent_factory or self._default_agent_factory
self.runtime_instance_id = runtime_instance_id or str(uuid.uuid4())
self.host_registry = host_registry
self._route_health = RouteHealthBook()
self._provider_health = RouteHealthBook()
self._pool = _SmoothWeightedRoundRobin()
@@ -464,6 +470,7 @@ class EvoModelRuntime:
) -> PreparedRunQuote:
self.admission_verifier.require_preparation(grant)
self._validate_preparation_echo(grant, agent_input)
await self._validate_continuation(grant, agent_input, host)
tool_payload = self._tool_registry_payload(host)
request_key = (grant.subject_id, grant.request_id, grant.turn_id)
request_digest = self._preparation_request_digest(
@@ -616,7 +623,14 @@ class EvoModelRuntime:
for routes in purpose_routes.values()
for route in routes
}
execution_id = str(uuid.uuid4())
snapshot_payload = {
"execution_id": execution_id,
"predecessor_execution_id": grant.predecessor_execution_id,
"predecessor_checkpoint_id": grant.predecessor_checkpoint_id,
"predecessor_owner_epoch": grant.predecessor_owner_epoch,
"continuation_pending_hash": grant.continuation_pending_hash,
"continuation_decision_hash": grant.continuation_decision_hash,
"request_id": grant.request_id,
"turn_id": grant.turn_id,
"thread_id": grant.thread_id,
@@ -640,6 +654,7 @@ class EvoModelRuntime:
preparation_id = str(uuid.uuid4())
issued_at = now_ms()
quote = self.quote_authority.sign_quote(
execution_id=execution_id,
preparation_id=preparation_id,
request_id=grant.request_id,
turn_id=grant.turn_id,
@@ -737,6 +752,142 @@ class EvoModelRuntime:
self._prepared_requests[request_key] = preparation_id
return quote
def inspect(self, execution_id: str, *, owner_epoch: int | None = None,
boot_id: str | None = None) -> dict:
if self.host_registry is None:
return {"execution_id": execution_id, "status": "unknown",
"resources_confirmed_exited": False, "source": "registry_disabled"}
state = self.host_registry.inspect_control(
execution_id, owner_epoch=owner_epoch, boot_id=boot_id)
run = next((h.run for h in self._prepared.values()
if h.run is not None and h.run.run_id == execution_id), None)
if run is not None:
state["terminal_error_code"] = run._terminal_error_code
return state
async def cancel(self, execution_id: str, *, reason: str,
owner_epoch: int | None = None, boot_id: str | None = None) -> str:
run = next((run for _, run in self._started_grants.values()
if run.run_id == execution_id), None)
if run is None:
raise EvoRuntimeError("EXECUTION_UNKNOWN")
if self.host_registry is not None:
self.host_registry.accept_cancel(
execution_id, owner_epoch=owner_epoch, boot_id=boot_id, reason=reason)
# Consume the committed intent before yielding. Later transfers do not
# revoke already accepted cancellation; the run owns its cleanup task.
run._request_cancel(reason)
return await run.wait_stopped(timeout=2.0)
async def recover_terminal(self, execution_id: str, *, sink: Any) -> str:
"""Explicit host repair of durable cleanup evidence; never executes Graph."""
registry = self.host_registry
intent = registry.terminal_intent(execution_id) if registry is not None else None
if registry is None or intent is None:
raise EvoRuntimeError("TERMINAL_CLEANUP_EVIDENCE_REQUIRED")
event = EvoRuntimeEvent(**intent['event'])
digest = hashlib.sha256(canonical_json_v1(asdict(event))).hexdigest()
if digest != intent['digest']:
raise EvoRuntimeError("EVO_EVENT_CONFLICT")
if intent['phase'] == 'prepared':
try:
confirmation = await sink.confirm(event.event_id, digest)
if confirmation == 'absent':
await sink.commit(event)
confirmation = await sink.confirm(event.event_id, digest)
except Exception as exc:
raise EvoRuntimeError("EVENT_COMMIT_INDETERMINATE") from exc
if confirmation != 'committed':
raise EvoRuntimeError("EVO_EVENT_CONFLICT" if confirmation == 'conflict'
else "EVENT_COMMIT_INDETERMINATE")
await asyncio.to_thread(registry.confirm_terminal, execution_id, digest=digest)
outcome = str(event.payload['outcome'])
await asyncio.to_thread(registry.finish, execution_id, outcome=outcome,
checkpoint_id=str(event.payload.get('checkpoint_id') or ''))
run = next((h.run for h in self._prepared.values()
if h.run is not None and h.run.run_id == execution_id), None)
if run is not None and run._terminal_task is not None and run._terminal_task.done():
async with run._condition:
if not any(item.event_id == event.event_id for item in run._journal):
run._journal.append(event)
run._journal_bytes += len(canonical_json_v1(asdict(event)))
run._sequence = max(run._sequence, event.sequence)
run._terminal_event = event
run._state = 'TERMINAL'
run._terminal_error_code = None
run._condition.notify_all()
if run._prepared.state == 'CONSTRUCTION_FAILED':
async with self._registry_lock:
self._retire_prepared_locked(run._prepared)
return outcome
async def _checkpoint_store_id(self, host):
# No portable durable identity exists in BaseCheckpointSaver. Only the
# inspected SQLite implementation is supported by this opt-in protocol.
from langgraph.checkpoint.sqlite.aio import AsyncSqliteSaver
import os
if type(host.checkpointer) is not AsyncSqliteSaver:
raise EvoRuntimeError("CHECKPOINT_STORE_UNSUPPORTED")
async with host.checkpointer.conn.execute("PRAGMA database_list") as cursor:
databases = await cursor.fetchall()
filename = next((row[2] for row in databases if row[1] == "main"), "")
if not filename:
raise EvoRuntimeError("CHECKPOINT_STORE_UNSUPPORTED")
info = os.stat(filename)
return f"sqlite:{info.st_dev}:{info.st_ino}"
async def _validate_continuation(self, grant, agent_input, host):
if self.host_registry is None:
return
from langgraph.types import Command
command = agent_input.message
if not grant.predecessor_execution_id:
if isinstance(command, Command):
raise EvoRuntimeError("CONTINUATION_REQUIRED")
return
pending = self.host_registry.continuation(grant.predecessor_execution_id)
if (pending["owner_epoch"] != grant.predecessor_owner_epoch
or isinstance(grant.predecessor_owner_epoch, bool)
or pending["pending_hash"] != grant.continuation_pending_hash
or pending["checkpoint_id"] != grant.predecessor_checkpoint_id
or grant.checkpoint_snapshot_id != grant.predecessor_checkpoint_id):
raise EvoRuntimeError("CONTINUATION_AUTHORIZATION_INVALID")
if not isinstance(command, Command) or command.update or command.goto or command.graph:
raise EvoRuntimeError("CONTINUATION_DECISION_INVALID")
resume = command.resume
if (not isinstance(resume, dict) or set(resume) != {"decisions"}
or hashlib.sha256(canonical_json_v1(resume)).hexdigest() != grant.continuation_decision_hash):
raise EvoRuntimeError("CONTINUATION_DECISION_INVALID")
intent = self.host_registry.terminal_intent(grant.predecessor_execution_id)
if intent is None or intent["phase"] != "registry_finished":
raise EvoRuntimeError("CONTINUATION_INVALID")
payload = intent["event"]["payload"]
identity = {k: payload[k] for k in (
"checkpoint_thread_id", "checkpoint_id", "checkpoint_ns", "pending_interrupts")}
if identity["checkpoint_thread_id"] != agent_input.checkpoint_thread_id or identity["checkpoint_ns"] != "":
raise EvoRuntimeError("CONTINUATION_INVALID")
checkpoint = await host.checkpointer.aget_tuple({"configurable": {
"thread_id": agent_input.checkpoint_thread_id, "checkpoint_ns": identity["checkpoint_ns"]}})
if (checkpoint is None or checkpoint.config["configurable"]["checkpoint_id"] != identity["checkpoint_id"]
or checkpoint.checkpoint["channel_values"].get("_verified_review_mode", {}).get("mode") != "manual"):
raise EvoRuntimeError("CONTINUATION_CHECKPOINT_STALE")
actual = []
for _, channel, value in checkpoint.pending_writes or ():
if channel == "__interrupt__":
for item in value:
projected = {"id": item.id, "value": item.value}
if projected not in actual:
actual.append(projected)
if actual != identity["pending_interrupts"] or not actual:
raise EvoRuntimeError("CONTINUATION_PENDING_STALE")
actions = [action for item in actual for action in item["value"].get("action_requests", [])]
decisions = resume["decisions"]
if (len(actual) != 1 or not actions or not isinstance(decisions, list)
or len(decisions) != len(actions)
or any(not isinstance(d, dict) or d.get("type") not in {"approve", "reject"}
or set(d) - {"type", "message"} for d in decisions)):
raise EvoRuntimeError("CONTINUATION_DECISION_INVALID")
async def start_web_run(self, admission: AdmissionGrant) -> EvoWebRun:
self.admission_verifier.require_admission(admission)
digest = canonical_json_v1(admission.unsigned_payload())
@@ -746,6 +897,14 @@ class EvoModelRuntime:
if replay[0] != digest:
raise EvoRuntimeError("CONTRACT_REPLAYED")
return replay[1]
if self.host_registry is not None:
binding = self.host_registry.lookup_grant(
admission.grant_id, hashlib.sha256(digest).hexdigest()
)
if binding is not None:
if self.host_registry.inspect(binding["execution_id"])["status"] == "failed":
raise EvoRuntimeError("EXECUTION_FAILED_RETRY_REQUIRES_NEW_ADMISSION")
raise EvoRuntimeError("EXECUTION_UNKNOWN")
handle = self._prepared.get(admission.preparation_id)
if handle is None:
raise EvoRuntimeError("RUN_LOST")
@@ -761,22 +920,59 @@ class EvoModelRuntime:
self._validate_still_fresh(handle)
if handle.host.runtime_event_sink is None:
raise EvoRuntimeError("EVENT_INGRESS_UNAVAILABLE")
model_set = self._build_model_set(
handle.snapshot, admission.reasoning_effort
)
agent = self.agent_factory(handle.snapshot, handle.host, model_set)
run = _EvoWebRun(
runtime=self,
admission=admission,
prepared=handle,
agent=agent,
model_set=model_set,
agent=None,
model_set=AgentModelSet(None, None, None),
)
if self.host_registry is not None:
store_id = await self._checkpoint_store_id(handle.host)
self.host_registry.claim_checkpoint(
run.run_id, store_id=store_id,
checkpoint_thread_id=handle.input.checkpoint_thread_id,
subject_id=admission.subject_id)
try:
await self._validate_continuation(handle.grant, handle.input, handle.host)
if self.host_registry is not None:
self.host_registry.bind(
execution_id=run.run_id, grant_id=admission.grant_id,
digest=hashlib.sha256(digest).hexdigest(),
thread_id=admission.thread_id, turn_id=admission.turn_id,
predecessor_execution_id=handle.grant.predecessor_execution_id,
predecessor_checkpoint_id=handle.grant.predecessor_checkpoint_id,
predecessor_owner_epoch=handle.grant.predecessor_owner_epoch,
continuation_pending_hash=handle.grant.continuation_pending_hash,
continuation_decision_hash=handle.grant.continuation_decision_hash,
)
except BaseException:
if self.host_registry is not None:
self.host_registry.release_unbound_checkpoint(run.run_id)
raise
token = _construction_owner.set(run)
try:
model_set = self._build_model_set(
handle.snapshot, admission.reasoning_effort
)
run._model_set = model_set
run._agent = self.agent_factory(handle.snapshot, handle.host, model_set)
except BaseException:
# Retain the owner on the handle even if rollback itself fails.
handle.run = run
handle.state = "CONSTRUCTION_FAILED"
run._start_terminal("failed", error_code="RUN_CONSTRUCTION_FAILED")
await run._ensure_cleanup()
await run.wait_stopped(timeout=0.25)
raise
finally:
_construction_owner.reset(token)
handle.state = "STARTED"
handle.run = run
handle.start_grant_digest = digest
async with self._registry_lock:
self._started_grants[admission.grant_id] = (digest, run)
run._start()
return run
async def cancel_prepared_run(self, preparation_id: str, *, reason: str) -> bool:
@@ -1089,7 +1285,7 @@ class EvoModelRuntime:
invocation_identity = route_fingerprint(config, route, fingerprint_key)
supports_tools = bool(model.capabilities.get("tools", False))
effective_tool_transport = (
route.tool_call_transport if supports_tools else "disabled"
"native" if supports_tools else "disabled"
)
identity = RouteIdentity(
config_revision=config.config_revision,
@@ -1269,6 +1465,14 @@ class EvoModelRuntime:
provider=route.runtime_provider,
**self._model_factory_kwargs(route),
)
if isinstance(model, ModelFactoryResult):
owner = _construction_owner.get()
if owner is None and model.owned_clients:
raise EvoRuntimeError("MODEL_RESOURCE_OWNER_MISSING")
if owner is not None:
for client in model.owned_clients:
owner._owned_clients[id(client)] = client
model = model.model
return self._attach_route_metadata(model, route, purpose, bound)
@staticmethod
@@ -1355,13 +1559,10 @@ class EvoModelRuntime:
invocation_plan=plan,
)
params.pop("output_token_limit", None)
params.update(
{
"max_tokens": bound.max_output_tokens,
"disable_streaming": "tool_calling",
"streaming": False
if purpose != "main_agent"
else params.get("streaming", False),
"use_responses_api": route.identity.api_mode == "responses",
}
)
@@ -1645,8 +1846,26 @@ class EvoModelRuntime:
from .gemini_interactions import create_gemini_interactions_model
return create_gemini_interactions_model(**kwargs)
from .models import get_chat_model
from .models import _OPENAI_ROUTED_PROVIDERS, get_chat_model
# LangChain caches default OpenAI HTTP clients process-wide. A run must
# never close that cache underneath another run's active stream.
if kwargs.get("provider") == "openai" or kwargs.get("provider") in _OPENAI_ROUTED_PROVIDERS:
import httpx
# Supplying owned clients disables LangChain's automatic usage opt-in.
# Keep explicit caller configuration and borrowed transports intact.
kwargs.setdefault("stream_usage", True)
if "http_client" not in kwargs:
kwargs["http_client"] = httpx.Client()
owner = _construction_owner.get()
if owner is not None:
owner._owned_clients[id(kwargs["http_client"])] = kwargs["http_client"]
if "http_async_client" not in kwargs:
kwargs["http_async_client"] = httpx.AsyncClient()
owner = _construction_owner.get()
if owner is not None:
owner._owned_clients[id(kwargs["http_async_client"])] = kwargs["http_async_client"]
return get_chat_model(**kwargs)
@staticmethod
@@ -1676,7 +1895,7 @@ class _EvoWebRun:
self._host = prepared.host
self._agent = agent
self._model_set = model_set
self._run_id = str(uuid.uuid4())
self._run_id = prepared.quote.execution_id or str(uuid.uuid4())
self._state = "ACTIVE"
self._sequence = 0
self._journal: list[EvoRuntimeEvent] = []
@@ -1686,6 +1905,12 @@ class _EvoWebRun:
self._budget_lock = asyncio.Lock()
self._terminal_lock = asyncio.Lock()
self._agent_task: asyncio.Task[None] | None = None
self._checkpoint_writes_stopped = True
from ..stream.stop import StreamStop
self._stream_stop = StreamStop() if runtime.host_registry is not None else None
self._event_iterator = None
self._stop_task = None
self._stop_errors = []
self._stream_wakeup_task: asyncio.Task[None] | None = None
self._attempt_counts: dict[str, int] = defaultdict(int)
self._remaining = admission.provider_run_reserve_microunits
@@ -1698,6 +1923,78 @@ class _EvoWebRun:
)
self._attempt_callback = _RuntimeAttemptCallback(self)
self._terminal_event: EvoRuntimeEvent | None = None
self._terminal_task: asyncio.Task[EvoRuntimeEvent] | None = None
self._terminal_error_code: str | None = None
self._terminal_phase = "cleanup"
self._cancel_task: asyncio.Task[None] | None = None
self._cleanup_task: asyncio.Task[None] | None = None
self._resource_close_tasks: dict[int, asyncio.Task[None]] = {}
self._owned_clients: dict[int, Any] = {}
self._cleanup_failures: dict[int, list[BaseException]] = defaultdict(list)
async def _close_owned_client(self, key: int, client: Any) -> None:
# A pending attempt owns the resource until it really returns. In
# particular, cancelling an observer cannot stop a synchronous thread.
for attempt in range(3):
try:
close = getattr(client, "aclose", None) or getattr(client, "close")
if asyncio.iscoroutinefunction(close):
result = close()
else:
result = await asyncio.to_thread(close)
if hasattr(result, "__await__"):
await result
except BaseException as exc:
self._cleanup_failures[key].append(exc)
if isinstance(exc, asyncio.CancelledError):
return
else:
self._owned_clients.pop(key, None)
return
if attempt < 2:
await asyncio.sleep(0)
async def _close_owned_clients(self) -> None:
self._resource_close_tasks = {
key: asyncio.create_task(
self._close_owned_client(key, client),
name=f"evo-resource-close-{self._run_id}-{key}",
)
for key, client in tuple(self._owned_clients.items())
}
if self._resource_close_tasks:
await asyncio.gather(*self._resource_close_tasks.values())
async def _ensure_cleanup(self, *, timeout: float | None = 0.25) -> None:
if self._cleanup_task is None:
self._cleanup_task = asyncio.create_task(
self._close_owned_clients(), name=f"evo-cleanup-{self._run_id}"
)
# asyncio.wait bounds observation without cancelling the run-owned
# coordinator or any close operation, including cancellation-resistant
# async clients. Later observers can confirm eventual completion.
await asyncio.wait({self._cleanup_task}, timeout=timeout)
if not self._cleanup_task.done():
raise TimeoutError("Run resource close remains unconfirmed")
self._cleanup_task.result()
if self._owned_clients:
errors = []
for key in self._owned_clients:
task = self._resource_close_tasks.get(key)
failures = self._cleanup_failures.get(key, [])
errors.append(
failures[-1] if task is not None and task.done() and failures
else TimeoutError("Run resource close remains unconfirmed")
)
raise BaseExceptionGroup("Run resource cleanup unconfirmed", errors)
def _start(self) -> None:
if self._agent_task is None and self._terminal_event is None:
self._agent_task = asyncio.create_task(
self._run_with_timeout(), name=f"evo-run-{self._run_id}"
)
self._agent_task.add_done_callback(self._wake_stream_waiters)
@property
def run_id(self) -> str:
@@ -1714,11 +2011,7 @@ class _EvoWebRun:
cursor = 0 if after_sequence is None else after_sequence
if cursor < 0 or cursor > self._sequence:
raise EvoRuntimeError("EVENT_CURSOR_INVALID")
if self._agent_task is None:
self._agent_task = asyncio.create_task(
self._run_with_timeout(), name=f"evo-run-{self._run_id}"
)
self._agent_task.add_done_callback(self._wake_stream_waiters)
async for event in self._replay(cursor):
yield event
@@ -1729,15 +2022,48 @@ class _EvoWebRun:
async with self._condition:
self._condition.notify_all()
async def cancel(self, reason: str) -> str:
async def cancel(self, reason: str, *, owner_epoch: int | None = None,
boot_id: str | None = None) -> str:
if self._runtime.host_registry is not None:
return await self._runtime.cancel(
self.run_id, reason=reason, owner_epoch=owner_epoch, boot_id=boot_id)
self._request_cancel(reason)
return await self.wait_stopped(timeout=2.0)
def _request_cancel(self, reason: str) -> None:
if self._terminal_task is not None or self._terminal_event is not None:
return
if self._cancel_task is None:
self._state = "CANCELLING"
if self._agent_task is not None and not self._agent_task.done():
self._agent_task.cancel()
self._cancel_task = asyncio.create_task(
self._finish_cancel(reason), name=f"evo-stop-{self._run_id}"
)
async def wait_stopped(self, timeout: float | None = 2.0) -> str:
if self._terminal_event is not None:
return str(self._terminal_event.payload.get("outcome") or "terminal")
try:
async with asyncio.timeout(timeout):
task = self._cancel_task or self._agent_task
if task is not None:
await asyncio.shield(task)
if self._terminal_task is not None:
await asyncio.shield(self._terminal_task)
except Exception:
# Observation is not an ownership transfer or a stop proof.
return "unknown"
if self._terminal_event is None:
return "unknown"
return str(self._terminal_event.payload.get("outcome") or "terminal")
async def _finish_cancel(self, reason: str) -> None:
if self._agent_task is not None:
self._agent_task.cancel()
await asyncio.gather(self._agent_task, return_exceptions=True)
await self._terminal_locked(
"cancelled", error_code="RUN_CANCELLED", reason=reason
)
return "cancelled"
async def _run_with_timeout(self) -> None:
try:
@@ -1763,9 +2089,13 @@ class _EvoWebRun:
"billing_fencing_token": self._admission.billing_fencing_token,
},
)
from ..stream.events import stream_agent_events
from ..stream.events import (
_snapshot_has_pending_interrupt,
stream_agent_events,
)
async for source in stream_agent_events(
self._checkpoint_writes_stopped = False
self._event_iterator = stream_agent_events(
self._agent,
self._input.message,
self._input.checkpoint_thread_id,
@@ -1782,21 +2112,66 @@ class _EvoWebRun:
configurable={
"turn_fencing_token": self._admission.turn_fencing_token,
"turn_lease_owner": self._admission.request_id,
"checkpoint_writer_strict_close": self._runtime.host_registry is not None,
**({"checkpoint_id": self._prepared.grant.predecessor_checkpoint_id,
"checkpoint_ns": ""}
if self._prepared.grant.predecessor_execution_id else {}),
},
error_mode="raise",
):
if str(source.get("type") or "") == "done":
continue
await self._append_locked("agent", dict(source))
**({"stop_owner": self._stream_stop} if self._stream_stop else {}),
)
try:
async for source in self._event_iterator:
if str(source.get("type") or "") == "done":
continue
await self._append_locked("agent", dict(source))
finally:
if self._stop_task is None:
self._stop_task = asyncio.create_task(
self._stop_checkpoint_writer(), name=f"evo-writer-stop-{self._run_id}")
await asyncio.shield(self._stop_task)
try:
final_state = await self._agent.aget_state(
{"configurable": {"thread_id": self._input.checkpoint_thread_id}}
)
checkpoint = final_state.config["configurable"]
if not checkpoint.get("checkpoint_id"):
raise ValueError("Final checkpoint identity missing")
pending = []
for item in (
*getattr(final_state, "interrupts", ()),
*(item for task in getattr(final_state, "tasks", ())
for item in getattr(task, "interrupts", ())),
):
projected = {"id": item.id, "value": item.value}
if projected not in pending:
pending.append(projected)
checkpoint_details = json.loads(json.dumps({
"checkpoint_thread_id": checkpoint["thread_id"],
"checkpoint_id": checkpoint["checkpoint_id"],
"checkpoint_ns": checkpoint.get("checkpoint_ns", ""),
"pending_interrupts": pending,
}, allow_nan=False))
awaiting_input = _snapshot_has_pending_interrupt(final_state)
except Exception:
await self._terminal_locked(
"failed", error_code="FINAL_CHECKPOINT_READ_FAILED"
)
return
if awaiting_input:
await self._terminal_locked(
"awaiting_input", checkpoint_details=checkpoint_details
)
return
if self._admission.title_policy == "best_effort":
await self._run_title()
else:
await self._append_locked(
"title", {"kind": "skipped", "reason": "DISABLED"}
)
await self._terminal_locked("completed")
await self._terminal_locked("completed", checkpoint_details=checkpoint_details)
except asyncio.CancelledError:
return
raise
except Exception as exc:
logger.exception(
"evo run %s failed before model provider error was reported",
@@ -1812,6 +2187,21 @@ class _EvoWebRun:
error_details=error_details,
)
async def _stop_checkpoint_writer(self) -> None:
while True:
try:
if self._event_iterator is not None:
await self._event_iterator.aclose()
if self._stream_stop is not None:
await asyncio.shield(self._stream_stop.start([]))
from ..stream.stop import sqlite_barrier
await sqlite_barrier(self._host.checkpointer)
self._checkpoint_writes_stopped = True
return
except Exception as exc:
self._stop_errors.append(exc)
await asyncio.sleep(0.05)
async def _run_title(self) -> None:
if self._model_set.title is None:
await self._append_locked(
@@ -2048,6 +2438,9 @@ class _EvoWebRun:
payload_digest = hashlib.sha256(
canonical_json_v1(asdict(event))
).hexdigest()
registry = self._runtime.host_registry
if control_terminal and registry is not None:
await asyncio.to_thread(registry.prepare_terminal, self.run_id, event=asdict(event))
commit_error: Exception | None = None
try:
result = await sink.commit(event)
@@ -2069,6 +2462,8 @@ class _EvoWebRun:
raise EvoRuntimeError("EVO_EVENT_CONFLICT")
else:
raise EvoRuntimeError("EVENT_COMMIT_INDETERMINATE")
if control_terminal and registry is not None:
await asyncio.to_thread(registry.confirm_terminal, self.run_id, digest=payload_digest)
async with self._condition:
self._sequence = sequence
self._journal_bytes += event_size
@@ -2077,13 +2472,65 @@ class _EvoWebRun:
return event
async def _terminal_locked(
self,
outcome: str,
**details: Any,
) -> EvoRuntimeEvent:
# Freeze the first terminal intent before yielding. Agent timeout/cancel
# cannot cancel its compensation or substitute a different outcome.
return await asyncio.shield(self._start_terminal(outcome, **details))
def _start_terminal(self, outcome: str, **details: Any) -> asyncio.Task[EvoRuntimeEvent]:
if self._terminal_task is None:
self._terminal_task = asyncio.create_task(
self._finalize_terminal(outcome, **details),
name=f"evo-terminal-{self._run_id}",
)
self._terminal_task.add_done_callback(self._observe_terminal)
return self._terminal_task
def _observe_terminal(self, task: asyncio.Task[EvoRuntimeEvent]) -> None:
error = None if task.cancelled() else task.exception()
if task.cancelled() or error is not None:
self._terminal_error_code = (
"RUN_TERMINAL_CANCELLED" if task.cancelled() else
"RUN_TERMINAL_CLEANUP_UNCONFIRMED" if self._terminal_phase == "cleanup" else
"RUN_TERMINAL_COMMIT_UNCONFIRMED")
logger.error("owner terminal task failed: %s", self._terminal_error_code)
async def _finish_registry(self, outcome: str, checkpoint_id: str = "") -> None:
delay = 0.1
while self._runtime.host_registry is not None:
try:
await asyncio.to_thread(
self._runtime.host_registry.finish, self.run_id,
outcome=outcome, checkpoint_id=checkpoint_id,
)
return
except Exception:
# Product-owned compensation, never an Agent retry. Keep the
# claim and fixed outcome until the original owner confirms it.
logger.warning("registry finalization pending for %s", self.run_id)
await asyncio.sleep(delay)
delay = min(delay * 2, 5.0)
async def _finalize_terminal(
self,
outcome: str,
*,
error_code: str | None = None,
reason: str | None = None,
error_details: Mapping[str, Any] | None = None,
checkpoint_details: Mapping[str, Any] | None = None,
) -> EvoRuntimeEvent:
# Terminalization is run-owned. Only public observers have deadlines;
# a slow close must not permanently fail the finalization task.
if self._stop_task is not None:
await asyncio.shield(self._stop_task)
await self._ensure_cleanup(timeout=None)
if self._runtime.host_registry is not None and not self._checkpoint_writes_stopped:
raise EvoRuntimeError("CHECKPOINT_WRITER_STOP_UNCONFIRMED")
self._terminal_phase = "commit"
async with self._terminal_lock:
if self._terminal_event is not None:
return self._terminal_event
@@ -2103,9 +2550,15 @@ class _EvoWebRun:
"error_details": dict(error_details or {}),
"purpose_attempt_counts": dict(self._attempt_counts),
"provider_reserve_remaining_microunits": self._remaining,
**dict(checkpoint_details or {}),
},
control_terminal=True,
)
await self._finish_registry(
outcome, str((checkpoint_details or {}).get("checkpoint_id") or ""))
if self._prepared.state == "CONSTRUCTION_FAILED":
async with self._runtime._registry_lock:
self._runtime._retire_prepared_locked(self._prepared)
async with self._condition:
self._state = "TERMINAL"
self._terminal_event = event
@@ -2155,7 +2608,13 @@ class _RuntimeAttemptCallback(AsyncCallbackHandler):
purpose, route = self._route_for(metadata)
try:
callback_payload = _callback_messages_payload(messages)
input_bound = _provider_input_token_bound(callback_payload)
# Structured output and bound tools live in invocation params, not messages.
invocation = _kwargs.get("invocation_params", {})
input_parameters = _effective_callback_input_parameters(invocation)
input_bound = _provider_input_token_bound({
"messages": callback_payload,
**input_parameters,
})
if purpose == "main_agent" and self.run._force_context_repair_pending:
self.run._force_context_repair_pending = False
raise EvoRuntimeError(
@@ -2535,6 +2994,12 @@ class _ProviderInputBound:
def _decode_media_payload(payload: str, mime: str) -> tuple[bytes, str]:
if payload.startswith("data:"):
# Bound the encoded allocation before split/decode using the same image
# ingress limit, not the ordinary JSON/schema text budget.
from EvoScientist.document_extract import MAX_IMAGE_BYTES
if len(payload) > 4 * ((MAX_IMAGE_BYTES + 2) // 3) + 256:
raise EvoRuntimeError("TOKEN_BOUND_UNAVAILABLE")
try:
header, encoded = payload.split(",", 1)
resolved_mime = header[5:].split(";", 1)[0] or mime
@@ -2640,7 +3105,7 @@ def _project_media_for_bound(value: Any, metrics: dict[str, int]) -> Any:
def _provider_input_token_bound(payload: Any) -> _ProviderInputBound:
metrics = {"media_tokens": 0, "media_blocks": 0, "largest_media_bytes": 0}
projected = _project_media_for_bound(payload, metrics)
projected = _callback_input_parameters(payload, media_metrics=metrics)
text_tokens = _payload_token_bound(projected)
media_tokens = metrics["media_tokens"]
return _ProviderInputBound(
@@ -2652,6 +3117,170 @@ def _provider_input_token_bound(payload: Any) -> _ProviderInputBound:
)
_INPUT_PROJECTION_MAX_DEPTH = 64
_INPUT_PROJECTION_MAX_NODES = 100_000
_INPUT_PROJECTION_MAX_BYTES = 8 * 1024 * 1024
def _effective_callback_input_parameters(invocation: Any) -> dict[str, Any]:
"""Project already-flattened OpenAI callback params, then SDK shallow merge.
ChatOpenAI._default_params expands model_kwargs; _get_invocation_params
overlays call kwargs. OpenAI BaseClient._build_request uses _merge_mappings
with extra_json last (not a recursive merge). A remaining model_kwargs is
therefore NOT a second defaults layer we can safely interpret.
Unknown body extensions and explicit alternative context transports fail closed.
Responses use_previous_response_id may generate an ID after this callback;
that SDK-managed context is outside this projection's visibility, not banned.
"""
fields = {
"tools", "functions", "response_format", "tool_choice", "function_call",
"system", "instructions",
}
if type(invocation) is not dict or any(type(key) is not str for key in invocation):
raise EvoRuntimeError("MODEL_INPUT_PROJECTION_INVALID")
if any(key in invocation for key in (
"model_kwargs", "messages", "input", "text", "previous_response_id",
"conversation", "prompt",
)):
raise EvoRuntimeError("MODEL_INPUT_PROJECTION_INVALID")
extra = invocation.get("extra_body")
if extra is None:
extra = {}
# These non-input controls are emitted by adapter_registry/models. Validate
# their data without dropping or altering them in the actual SDK request.
controls = {"enable_thinking", "thinking_budget", "thinking"}
if type(extra) is not dict or any(
type(key) is not str or key not in fields | controls for key in extra
):
raise EvoRuntimeError("MODEL_INPUT_PROJECTION_INVALID")
effective = {key: invocation[key] for key in fields if key in invocation}
effective.update(extra)
validated = _callback_input_parameters(effective)
return {key: value for key, value in validated.items() if key in fields}
def _callback_input_parameters(
value: Any, *, media_metrics: dict[str, int] | None = None
) -> Any:
"""Validate JSON/schema trees before serialization; never format rejected values.
Limits cover depth (root=0), visited nodes including keys, and aggregate
JSON scalar/container bytes. Shared acyclic values are counted each time.
BaseModel classes are trusted application code: schema hooks execute before
their output can be bounded. This is not a sandbox for untrusted classes.
"""
from pydantic import BaseModel
active: set[int] = set()
nodes = 0
size = 0
def visit(item: Any, depth: int, path: tuple[str, ...] = (),
media: tuple[tuple[str, ...], str] | None = None) -> Any:
nonlocal nodes, size
nodes += 1
if depth > _INPUT_PROJECTION_MAX_DEPTH or nodes > _INPUT_PROJECTION_MAX_NODES:
raise ValueError
kind = type(item)
if item is None or kind in (bool, int, float, str):
if kind is str and media is not None and path == media[0]:
from EvoScientist.document_extract import MAX_IMAGE_BYTES, prepare_image_bytes
if len(item) > 4 * ((MAX_IMAGE_BYTES + 2) // 3) + 256:
raise ValueError
raw, mime = _decode_media_payload(item, media[1])
if not mime.startswith("image/"):
raise ValueError
# Parse using the ingress pixel/byte limits. Do not replace the
# actual request with the downsampled result or with the marker.
prepare_image_bytes(raw, "callback-image")
assert media_metrics is not None
media_metrics["media_tokens"] += (len(raw) + 2) // 3 + 512
media_metrics["media_blocks"] += 1
media_metrics["largest_media_bytes"] = max(
media_metrics["largest_media_bytes"], len(raw)
)
item = f"<media:{mime}:sha256:{hashlib.sha256(raw).hexdigest()[:24]}>"
elif kind is str and media_metrics is not None and item.startswith("data:"):
raise ValueError
if isinstance(item, float) and not math.isfinite(item):
raise ValueError
if isinstance(item, str) and len(item) > _INPUT_PROJECTION_MAX_BYTES:
raise ValueError
size += len(json.dumps(item, ensure_ascii=True, allow_nan=False))
if size > _INPUT_PROJECTION_MAX_BYTES:
raise ValueError
return item
is_schema = isinstance(item, type) and issubclass(item, BaseModel)
if kind not in (dict, list) and not is_schema:
raise ValueError
identity = id(item)
if identity in active:
raise ValueError
active.add(identity)
try:
if isinstance(item, type) and issubclass(item, BaseModel):
return visit(item.model_json_schema(), depth + 1)
if not isinstance(item, (dict, list)):
raise ValueError
if len(item) > _INPUT_PROJECTION_MAX_NODES - nodes:
raise ValueError
size += 2 + 2 * len(item)
if size > _INPUT_PROJECTION_MAX_BYTES:
raise ValueError
if isinstance(item, list):
return [visit(child, depth + 1, path + ("[]",)) for child in item]
# Only content-list image blocks (or a direct block-list caller) can
# exempt payload bytes. Tools/schema objects never acquire this role.
content_block = path in {
("[]",),
("messages", "[]", "[]", "data", "content", "[]"),
("messages", "[]", "content", "[]"),
}
if media_metrics is not None and content_block:
block_type = item.get("type")
mime = item.get("mime_type", "image/unknown")
if type(item.get("inline_data")) is dict:
inline_mime = item["inline_data"].get("mime_type")
if type(inline_mime) is str and inline_mime.startswith("image/"):
media = (path + ("inline_data", "data"), inline_mime)
if type(block_type) is str and block_type in {"image", "image_url", "input_image"}:
location = None
if type(item.get("base64")) is str:
location = ("base64",)
elif (type(item.get("image_url")) is dict
and type(item["image_url"].get("url")) is str
and item["image_url"]["url"].startswith("data:")):
location = ("image_url", "url")
elif (type(item.get("image_url")) is str
and item["image_url"].startswith("data:")):
location = ("image_url",)
elif type(item.get("url")) is str and item["url"].startswith("data:"):
location = ("url",)
elif type(item.get("source")) is dict and item["source"].get("type") == "base64":
location = ("source", "data")
mime = item["source"].get("media_type", mime)
if location is not None:
if type(mime) is not str:
raise ValueError
media = (path + location, mime)
result = {}
for key, child in item.items():
if type(key) is not str:
raise ValueError
visit(key, depth + 1)
result[key] = visit(child, depth + 1, path + (key,), media)
return result
finally:
active.remove(identity)
try:
return visit(value, 0)
except Exception:
raise EvoRuntimeError("MODEL_INPUT_PROJECTION_INVALID") from None
def _callback_messages_payload(messages: Sequence[Sequence[Any]]) -> list[list[Any]]:
"""Convert LangChain callback messages to canonical-JSON-safe values."""