Files
m4 5a581c78a2
Build / build (push) Has been cancelled
Docker / build (push) Has been cancelled
Lint / ruff (push) Has been cancelled
Test / pytest (ubuntu-latest, 3.11) (push) Has been cancelled
Test / pytest (ubuntu-latest, 3.12) (push) Has been cancelled
Test / pytest (windows-latest, 3.11) (push) Has been cancelled
Test / pytest (windows-latest, 3.12) (push) Has been cancelled
feat: add scoped model runtime configuration
Introduce provider, model, and invocation contracts with encrypted configuration persistence. Add web runtime fencing, route fallback, recovery middleware, workspace scoping, and comprehensive tests.
2026-08-14 22:03:04 +08:00

423 lines
14 KiB
Python

"""EvoMemory LangGraph launch adapter."""
from __future__ import annotations
import json
from collections.abc import Callable, Mapping
from pathlib import Path
from typing import cast
from ..config import MemoryControls, get_effective_config
from ..gateway.background_runs import (
BackgroundRun,
BackgroundRunHooks,
BackgroundRunPayload,
BackgroundRunRequest,
alaunch_background_run,
launch_background_run,
)
from ..langgraph_dev.sdk import messages_input
from .observations import build_observation_linker_index_context
from .scheduler import ObservationLinkerContext
from .source_context import MemorySourceContext, _trajectory_for_prompt
from .types import MemorySourceType
from .worker_activity import (
MemoryOutputDelta,
MemoryOutputSnapshot,
ObservationRelationSnapshot,
forget_memory_worker,
forget_observation_linker,
mark_memory_worker_finished,
mark_memory_worker_started,
mark_observation_linker_finished,
mark_observation_linker_started,
snapshot_memory_outputs,
snapshot_observation_relations,
)
SUBAGENT_MEMORY_WORKER_GRAPH_ID = "evomemory-subagent-worker"
TURN_MEMORY_WORKER_GRAPH_ID = "evomemory-turn-worker"
OBSERVATION_LINKER_GRAPH_ID = "evomemory-observation-linker"
MemoryWorkerFinishedHook = Callable[[BackgroundRun, MemoryOutputDelta | None], None]
MemoryWorkerAbortedHook = Callable[[BackgroundRun, MemoryOutputDelta | None], None]
_INHERITED_RUNTIME_KEYS = (
"model",
"model_provider",
"ai4sci_metering",
"ai4sci_model_proxy",
)
def _current_runtime_context() -> tuple[str | None, dict[str, object]]:
"""Capture the signed parent Run context before launching a child graph."""
try:
from langgraph.config import get_config
config = get_config()
except Exception:
return None, {}
if not isinstance(config, Mapping):
return None, {}
configurable = config.get("configurable")
inherited = {
key: configurable[key]
for key in _INHERITED_RUNTIME_KEYS
if isinstance(configurable, Mapping) and key in configurable
}
metadata = config.get("metadata")
runtime_url = (
str(metadata.get("langgraph_api_url") or "")
if isinstance(metadata, Mapping)
else ""
)
return runtime_url or None, inherited
def _with_runtime_context(
payload: BackgroundRunPayload,
*,
inherited: Mapping[str, object] | None,
source_type: str,
) -> BackgroundRunPayload:
"""Attach a child billing scope without changing the signed envelope."""
normalized = cast("BackgroundRunPayload", dict(payload))
config = dict(normalized.get("config") or {})
configurable = dict(config.get("configurable") or {})
configurable.update(dict(inherited or {}))
metering = configurable.get("ai4sci_metering")
if isinstance(metering, Mapping):
configurable["ai4sci_metering"] = {
**dict(metering),
"source_type": source_type,
}
config["configurable"] = configurable
return cast("BackgroundRunPayload", {**normalized, "config": config})
def _observation_linking_enabled() -> bool:
return MemoryControls.from_config(get_effective_config()).observations_enabled
def _memory_worker_graph_id(source_type: MemorySourceType) -> str:
match source_type:
case MemorySourceType.TURN:
return TURN_MEMORY_WORKER_GRAPH_ID
case MemorySourceType.SUBAGENT:
return SUBAGENT_MEMORY_WORKER_GRAPH_ID
case _:
raise ValueError(f"Unsupported memory source type: {source_type!r}")
def _memory_worker_user_prompt(context: MemorySourceContext) -> str:
match context.source_type:
case MemorySourceType.TURN:
return (
"Review this completed orchestrator turn.\n\n"
f"Source agent: {context.source_agent}\n"
f"Source session: {context.session_id}\n\n"
f"Turn trajectory:\n{_trajectory_for_prompt(context.trajectory)}"
)
case MemorySourceType.SUBAGENT:
return (
"Review this completed subagent run.\n\n"
f"Source agent: {context.source_agent}\n"
f"Source session: {context.session_id}\n\n"
f"Trajectory:\n{_trajectory_for_prompt(context.trajectory)}"
)
case _:
raise ValueError(f"Unsupported memory source type: {context.source_type!r}")
def _runs_create_kwargs(payload: BackgroundRunPayload) -> BackgroundRunPayload:
try:
from EvoScientist.llm.patches import _merge_runs_config_kwargs
except Exception:
return payload
return cast("BackgroundRunPayload", _merge_runs_config_kwargs(dict(payload)))
def _worker_workspace_dir(workspace_dir: str | Path) -> str:
return str(Path(workspace_dir).expanduser().resolve())
def _memory_worker_metadata(context: MemorySourceContext) -> dict[str, str]:
return {
"run_kind": f"evomemory_{context.source_type.value}_worker",
"source_session_id": context.session_id,
"source_agent": context.source_agent,
"project_id": context.project_id,
"trajectory_digest": context.trajectory_digest,
"workspace_dir": _worker_workspace_dir(context.workspace_dir),
}
def _memory_worker_run_payload(
*,
context: MemorySourceContext,
thread_id: str,
inherited: Mapping[str, object] | None = None,
) -> BackgroundRunPayload:
"""Build the LangGraph SDK run payload for a memory worker."""
metadata = _memory_worker_metadata(context)
payload: BackgroundRunPayload = {
"assistant_id": _memory_worker_graph_id(context.source_type),
"input": messages_input(_memory_worker_user_prompt(context)),
"metadata": metadata,
"config": {
"configurable": {
"thread_id": thread_id,
"evomemory_source_session_id": context.session_id,
"evomemory_source_agent": context.source_agent,
"evomemory_project_id": context.project_id,
"evomemory_trajectory_digest": context.trajectory_digest,
}
},
}
payload = _runs_create_kwargs(payload)
source_type = (
"evomemory_turn_worker"
if context.source_type == MemorySourceType.TURN
else "evomemory_subagent_worker"
)
return _with_runtime_context(
payload,
inherited=inherited,
source_type=source_type,
)
def memory_worker_launch_request(
context: MemorySourceContext,
) -> BackgroundRunRequest:
"""Build the background run request for a memory worker."""
metadata = _memory_worker_metadata(context)
runtime_url, inherited = _current_runtime_context()
def run_payload(thread_id: str) -> BackgroundRunPayload:
return _memory_worker_run_payload(
context=context,
thread_id=thread_id,
inherited=inherited,
)
return BackgroundRunRequest(
graph_id=_memory_worker_graph_id(context.source_type),
run_payload=run_payload,
thread_metadata=metadata,
url=runtime_url,
name="EvoMemory worker",
)
def _observation_linker_user_prompt(context: ObservationLinkerContext) -> str:
payload = {
"project_id": context.project_id,
"new_observation_ids": sorted(context.observation_ids),
}
prompt = (
"Link newly recorded observations when there is a strong reusable "
"relationship.\n\n"
f"{json.dumps(payload, ensure_ascii=False, indent=2, sort_keys=True)}"
)
observation_index = build_observation_linker_index_context(
memory_dir=context.memory_dir,
project_id=context.project_id,
exclude_ids=context.observation_ids,
)
if observation_index:
prompt += f"\n\n{observation_index}"
return prompt
def _observation_linker_metadata(
context: ObservationLinkerContext,
) -> dict[str, str]:
return {
"run_kind": "evomemory_observation_linker",
"project_id": context.project_id,
"observation_count": str(len(context.observation_ids)),
"workspace_dir": str(context.workspace_dir.expanduser().resolve()),
}
def _observation_linker_run_payload(
*,
context: ObservationLinkerContext,
thread_id: str,
) -> BackgroundRunPayload:
payload: BackgroundRunPayload = {
"assistant_id": OBSERVATION_LINKER_GRAPH_ID,
"input": messages_input(_observation_linker_user_prompt(context)),
"metadata": _observation_linker_metadata(context),
"config": {
"configurable": {
"thread_id": thread_id,
"evomemory_project_id": context.project_id,
"evomemory_observation_ids": json.dumps(
list(context.observation_ids),
ensure_ascii=False,
),
}
},
}
payload = _runs_create_kwargs(payload)
return _with_runtime_context(
payload,
inherited=context.runtime_configurable,
source_type="evomemory_linker",
)
def observation_linker_launch_request(
context: ObservationLinkerContext,
) -> BackgroundRunRequest:
"""Build the background run request for the observation linker."""
def run_payload(thread_id: str) -> BackgroundRunPayload:
return _observation_linker_run_payload(
context=context,
thread_id=thread_id,
)
return BackgroundRunRequest(
graph_id=OBSERVATION_LINKER_GRAPH_ID,
run_payload=run_payload,
thread_metadata=_observation_linker_metadata(context),
url=context.runtime_url,
name="EvoMemory observation linker",
)
def _observation_linker_launch_hooks(memory_dir: str | Path) -> BackgroundRunHooks:
before_relations: dict[str, ObservationRelationSnapshot] = {}
def on_before_run(_thread_id: str) -> None:
before_relations["value"] = snapshot_observation_relations(memory_dir)
def on_started(run: BackgroundRun) -> None:
mark_observation_linker_started(
thread_id=run.thread_id,
run_id=run.run_id,
before_relations=before_relations.get("value"),
)
def on_finished(run: BackgroundRun) -> None:
mark_observation_linker_finished(
run.thread_id,
run.run_id,
memory_dir=memory_dir,
)
def on_aborted(run: BackgroundRun) -> None:
forget_observation_linker(run.thread_id, run.run_id)
return BackgroundRunHooks(
on_before_run=on_before_run,
on_started=on_started,
on_finished=on_finished,
on_aborted=on_aborted,
on_watcher_start_failed=on_aborted,
)
def _memory_worker_launch_hooks(
memory_dir: str | Path,
*,
on_worker_finished: MemoryWorkerFinishedHook | None = None,
on_worker_aborted: MemoryWorkerAbortedHook | None = None,
) -> BackgroundRunHooks:
before_outputs: dict[str, MemoryOutputSnapshot] = {}
def on_before_run(_thread_id: str) -> None:
before_outputs["value"] = snapshot_memory_outputs(memory_dir)
def on_started(run: BackgroundRun) -> None:
mark_memory_worker_started(
thread_id=run.thread_id,
run_id=run.run_id,
memory_dir=memory_dir,
before_outputs=before_outputs.get("value"),
)
def on_finished(run: BackgroundRun) -> None:
delta = mark_memory_worker_finished(run.thread_id, run.run_id)
if on_worker_finished is not None:
on_worker_finished(run, delta)
def on_aborted(run: BackgroundRun) -> None:
delta = mark_memory_worker_finished(run.thread_id, run.run_id)
if on_worker_aborted is not None:
on_worker_aborted(run, delta)
def on_status_unknown(run: BackgroundRun) -> None:
forget_memory_worker(run.thread_id, run.run_id)
return BackgroundRunHooks(
on_before_run=on_before_run,
on_started=on_started,
on_finished=on_finished,
on_aborted=on_aborted,
on_status_unknown=on_status_unknown,
on_watcher_start_failed=on_aborted,
)
def launch_memory_worker(
context: MemorySourceContext,
*,
on_worker_finished: MemoryWorkerFinishedHook | None = None,
on_worker_aborted: MemoryWorkerAbortedHook | None = None,
) -> BackgroundRun | None:
"""Launch one synchronous EvoMemory worker for a source context."""
return launch_background_run(
memory_worker_launch_request(context),
hooks=_memory_worker_launch_hooks(
context.memory_dir,
on_worker_finished=on_worker_finished,
on_worker_aborted=on_worker_aborted,
),
)
async def alaunch_memory_worker(
context: MemorySourceContext,
*,
on_worker_finished: MemoryWorkerFinishedHook | None = None,
on_worker_aborted: MemoryWorkerAbortedHook | None = None,
) -> BackgroundRun | None:
"""Launch one asynchronous EvoMemory worker for a source context."""
return await alaunch_background_run(
memory_worker_launch_request(context),
hooks=_memory_worker_launch_hooks(
context.memory_dir,
on_worker_finished=on_worker_finished,
on_worker_aborted=on_worker_aborted,
),
)
def launch_observation_linker(
context: ObservationLinkerContext,
) -> BackgroundRun | None:
"""Launch one synchronous observation-linking pass."""
if not _observation_linking_enabled():
return None
return launch_background_run(
observation_linker_launch_request(context),
hooks=_observation_linker_launch_hooks(context.memory_dir),
)
async def alaunch_observation_linker(
context: ObservationLinkerContext,
) -> BackgroundRun | None:
"""Launch one asynchronous observation-linking pass."""
if not _observation_linking_enabled():
return None
return await alaunch_background_run(
observation_linker_launch_request(context),
hooks=_observation_linker_launch_hooks(context.memory_dir),
)