agentledger-runtime 1.0.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (49) hide show
  1. agentledger/__init__.py +183 -0
  2. agentledger/__main__.py +5 -0
  3. agentledger/adapters.py +62 -0
  4. agentledger/adapters_frameworks.py +146 -0
  5. agentledger/adapters_langgraph.py +140 -0
  6. agentledger/adapters_mcp.py +145 -0
  7. agentledger/approval.py +32 -0
  8. agentledger/backup.py +127 -0
  9. agentledger/blobstore.py +31 -0
  10. agentledger/blobstore_s3.py +118 -0
  11. agentledger/cli.py +1047 -0
  12. agentledger/conformance.py +468 -0
  13. agentledger/context.py +146 -0
  14. agentledger/contract.py +176 -0
  15. agentledger/cost.py +133 -0
  16. agentledger/diff.py +205 -0
  17. agentledger/eval.py +128 -0
  18. agentledger/evidence.py +285 -0
  19. agentledger/examples.py +62 -0
  20. agentledger/failure.py +190 -0
  21. agentledger/failure_injection.py +119 -0
  22. agentledger/ids.py +31 -0
  23. agentledger/jsonutil.py +31 -0
  24. agentledger/lint.py +415 -0
  25. agentledger/media.py +268 -0
  26. agentledger/media_tools.py +209 -0
  27. agentledger/policy.py +174 -0
  28. agentledger/protocol.py +32 -0
  29. agentledger/replay.py +74 -0
  30. agentledger/repro.py +335 -0
  31. agentledger/retention.py +111 -0
  32. agentledger/review.py +109 -0
  33. agentledger/runtime.py +187 -0
  34. agentledger/sandbox.py +749 -0
  35. agentledger/scheduler.py +53 -0
  36. agentledger/shadow.py +62 -0
  37. agentledger/simple.py +131 -0
  38. agentledger/storage_postgres.py +439 -0
  39. agentledger/storage_schema.py +368 -0
  40. agentledger/store.py +532 -0
  41. agentledger/timetravel.py +248 -0
  42. agentledger/tools.py +516 -0
  43. agentledger/trace.py +223 -0
  44. agentledger/worker.py +355 -0
  45. agentledger_runtime-1.0.0.dist-info/METADATA +264 -0
  46. agentledger_runtime-1.0.0.dist-info/RECORD +49 -0
  47. agentledger_runtime-1.0.0.dist-info/WHEEL +4 -0
  48. agentledger_runtime-1.0.0.dist-info/entry_points.txt +2 -0
  49. agentledger_runtime-1.0.0.dist-info/licenses/LICENSE +55 -0
@@ -0,0 +1,183 @@
1
+ """AgentLedger agent runtime v1.0 stable core."""
2
+
3
+ __version__ = "1.0.0"
4
+
5
+ from .adapters import FrameworkAdapter, PythonFunctionAdapter, python_agent
6
+ from .adapters_frameworks import AutoGenAdapter, CrewAIAdapter, LangChainRunnableAdapter, LlamaIndexAdapter, MethodFrameworkAdapter, OpenAIAgentsSDKAdapter, SemanticKernelAdapter
7
+ from .approval import ApprovalDecision, ApprovalRequired
8
+ from .adapters_langgraph import LangGraphCheckpointerAdapter, LangGraphNodeAdapter
9
+ from .adapters_mcp import InMemoryMCPContextServer, InMemoryMCPToolServer, MCPContextAdapter, MCPResourceDescriptor, MCPToolAdapter
10
+ from .backup import BackupCheck, BackupReadinessChecker, BackupReadinessReport
11
+ from .blobstore import LocalBlobStore
12
+ from .blobstore_s3 import S3BlobStore, S3BlobStoreConfig, S3DependencyMissing
13
+ from .conformance import BlobStoreConformanceRunner, ConformanceCheck, ConformanceReport, FrameworkAdapterConformanceRunner, MediaRuntimeConformanceRunner, StateStoreConformanceRunner, WorkerConformanceRunner
14
+ from .contract import CONTRACT_VERSION, contract_json, runtime_contract
15
+ from .diff import DiffReport, DivergenceReport, DivergenceReporter, EvidenceDiffer
16
+ from .context import AgentContext
17
+ from .cost import BudgetController, BudgetExceeded, BudgetLimits, CostAttributionReport, CostAttributionReporter
18
+ from .eval import EvidenceCheck, EvidenceCheckReport, EvidenceRegressionRunner
19
+ from .evidence import EvidenceExporter
20
+ from .failure import FailureAttributionReport, FailureAttributionReporter, FailureClassification, NonRetryableAgentError, RetryableAgentError, RetryPolicy
21
+ from .failure_injection import FailureInjectionCheck, FailureInjectionReport, FailureInjectionSuite
22
+ from .lint import BoundaryLintFinding, BoundaryLintReport, BoundaryLintRule, RuntimeBoundaryLinter, load_boundary_rules
23
+ from .media import ArtifactLineage, EventStreamCheckpoint, MediaArtifact, MediaMetadata, StreamChunkRef
24
+ from .media_tools import media_tool_specs, register_media_tool_conventions
25
+ from .policy import PolicyEngine, RolePolicy
26
+ from .protocol import BlobStoreProtocol, ModelProviderProtocol, StateStoreProtocol, ToolExecutorProtocol
27
+ from .repro import GoldenCase, GoldenCorpus
28
+ from .replay import ReplayEngine
29
+ from .runtime import Runtime, SimulatedCrash
30
+ from .retention import RetentionPlan, RetentionPlanner
31
+ from .review import AdversarialReviewReport, AdversarialReviewRunner, ReviewCheck
32
+ from .sandbox import BubblewrapSandboxExecutor, DisabledSandboxExecutor, DockerSandboxExecutor, E2BSandboxExecutor, FirecrackerSandboxExecutor, KubernetesSandboxExecutor, LocalSandboxExecutor, RemoteSandboxExecutor, SandboxConfig, SandboxExecutor, SandboxPolicy, SandboxResult, SandboxRouter, SandboxToolRule, SandboxUnavailable, create_sandbox_executor
33
+ from .simple import RunResult, SimpleAgent, agent, arun, run
34
+ from .scheduler import RecoverySummary, RuntimeScheduler
35
+ from .storage_schema import Migration, MigrationStatus, SQLiteMigrationRunner, ddl_for, latest_schema_version, migrations_for
36
+ from .storage_postgres import PostgresDependencyMissing, PostgresStore, PostgresStoreConfig
37
+ from .store import SQLiteStore
38
+ from .tools import ToolRegistry, ToolSpec, ToolValidationError, tool, validate_tool_schema
39
+ from .trace import OTLPResource, OTLPTraceExporter, TraceExporter, TraceSpan
40
+ from .timetravel import TimeTravelDebugger, TimeTravelFrame, TimeTravelReport
41
+ from .worker import LocalWorker, WorkerDeploymentPlan, WorkerRunSummary, WorkerService, WorkerServiceSummary, build_worker_deployment_plan
42
+
43
+ __all__ = [
44
+ "ApprovalDecision",
45
+ "ApprovalRequired",
46
+ "AdversarialReviewReport",
47
+ "AdversarialReviewRunner",
48
+ "ArtifactLineage",
49
+ "BackupCheck",
50
+ "BackupReadinessChecker",
51
+ "BackupReadinessReport",
52
+ "RunResult",
53
+ "SimpleAgent",
54
+ "RetentionPlan",
55
+ "RetentionPlanner",
56
+ "ReviewCheck",
57
+ "BubblewrapSandboxExecutor",
58
+ "DisabledSandboxExecutor",
59
+ "DockerSandboxExecutor",
60
+ "E2BSandboxExecutor",
61
+ "FirecrackerSandboxExecutor",
62
+ "KubernetesSandboxExecutor",
63
+ "RemoteSandboxExecutor",
64
+ "SandboxConfig",
65
+ "SandboxRouter",
66
+ "SandboxToolRule",
67
+ "SandboxUnavailable",
68
+ "create_sandbox_executor",
69
+ "SandboxExecutor",
70
+ "SandboxPolicy",
71
+ "SandboxResult",
72
+ "LocalSandboxExecutor",
73
+ "agent",
74
+ "arun",
75
+ "run",
76
+ "AgentContext",
77
+ "BlobStoreProtocol",
78
+ "BlobStoreConformanceRunner",
79
+ "BudgetController",
80
+ "BudgetExceeded",
81
+ "BudgetLimits",
82
+ "CostAttributionReport",
83
+ "CostAttributionReporter",
84
+ "BoundaryLintFinding",
85
+ "BoundaryLintReport",
86
+ "BoundaryLintRule",
87
+ "ConformanceCheck",
88
+ "ConformanceReport",
89
+ "CONTRACT_VERSION",
90
+ "__version__",
91
+ "EvidenceCheck",
92
+ "EvidenceCheckReport",
93
+ "EvidenceRegressionRunner",
94
+ "EventStreamCheckpoint",
95
+ "OTLPResource",
96
+ "OTLPTraceExporter",
97
+ "TraceSpan",
98
+ "TraceExporter",
99
+ "TimeTravelDebugger",
100
+ "TimeTravelFrame",
101
+ "TimeTravelReport",
102
+ "PostgresStoreConfig",
103
+ "PostgresStore",
104
+ "PostgresDependencyMissing",
105
+ "EvidenceDiffer",
106
+ "DiffReport",
107
+ "DivergenceReport",
108
+ "DivergenceReporter",
109
+ "EvidenceExporter",
110
+ "FailureInjectionCheck",
111
+ "FailureInjectionReport",
112
+ "FailureInjectionSuite",
113
+ "FailureClassification",
114
+ "FailureAttributionReport",
115
+ "FailureAttributionReporter",
116
+ "FrameworkAdapter",
117
+ "FrameworkAdapterConformanceRunner",
118
+ "AutoGenAdapter",
119
+ "CrewAIAdapter",
120
+ "GoldenCase",
121
+ "GoldenCorpus",
122
+ "LangGraphCheckpointerAdapter",
123
+ "LangGraphNodeAdapter",
124
+ "LangChainRunnableAdapter",
125
+ "LlamaIndexAdapter",
126
+ "LocalBlobStore",
127
+ "LocalWorker",
128
+ "MediaArtifact",
129
+ "MediaMetadata",
130
+ "media_tool_specs",
131
+ "MCPToolAdapter",
132
+ "MCPContextAdapter",
133
+ "MCPResourceDescriptor",
134
+ "InMemoryMCPToolServer",
135
+ "InMemoryMCPContextServer",
136
+ "MethodFrameworkAdapter",
137
+ "MediaRuntimeConformanceRunner",
138
+ "Migration",
139
+ "MigrationStatus",
140
+ "ModelProviderProtocol",
141
+ "NonRetryableAgentError",
142
+ "PolicyEngine",
143
+ "OpenAIAgentsSDKAdapter",
144
+ "PythonFunctionAdapter",
145
+ "RecoverySummary",
146
+ "ReplayEngine",
147
+ "RetryPolicy",
148
+ "RetryableAgentError",
149
+ "RolePolicy",
150
+ "Runtime",
151
+ "RuntimeBoundaryLinter",
152
+ "load_boundary_rules",
153
+ "RuntimeScheduler",
154
+ "SemanticKernelAdapter",
155
+ "S3BlobStore",
156
+ "S3BlobStoreConfig",
157
+ "S3DependencyMissing",
158
+ "SQLiteMigrationRunner",
159
+ "SQLiteStore",
160
+ "SimulatedCrash",
161
+ "StateStoreConformanceRunner",
162
+ "StateStoreProtocol",
163
+ "StreamChunkRef",
164
+ "ToolExecutorProtocol",
165
+ "ToolRegistry",
166
+ "ToolSpec",
167
+ "ToolValidationError",
168
+ "WorkerRunSummary",
169
+ "WorkerConformanceRunner",
170
+ "WorkerDeploymentPlan",
171
+ "WorkerService",
172
+ "WorkerServiceSummary",
173
+ "build_worker_deployment_plan",
174
+ "contract_json",
175
+ "ddl_for",
176
+ "latest_schema_version",
177
+ "migrations_for",
178
+ "python_agent",
179
+ "runtime_contract",
180
+ "register_media_tool_conventions",
181
+ "tool",
182
+ "validate_tool_schema",
183
+ ]
@@ -0,0 +1,5 @@
1
+ from .cli import main
2
+
3
+
4
+ if __name__ == "__main__":
5
+ main()
@@ -0,0 +1,62 @@
1
+ from __future__ import annotations
2
+
3
+ import inspect
4
+ from abc import ABC, abstractmethod
5
+ from typing import Any, Callable
6
+
7
+ from .context import AgentContext
8
+
9
+ AgentCallable = Callable[[AgentContext, dict[str, Any]], Any]
10
+
11
+
12
+ class FrameworkAdapter(ABC):
13
+ """Base contract for framework adapters.
14
+
15
+ Adapters map framework-specific concepts into AgentLedger's stable runtime
16
+ boundary. Core imports no LangGraph/CrewAI/AutoGen/etc dependencies.
17
+ """
18
+
19
+ name = "framework"
20
+
21
+ def map_run_spec(self, framework_run: Any) -> dict[str, Any]:
22
+ return {"adapter": self.name, "framework_run": repr(framework_run)}
23
+
24
+ def map_step(self, framework_step: Any) -> dict[str, Any]:
25
+ return {"adapter": self.name, "framework_step": repr(framework_step)}
26
+
27
+ @abstractmethod
28
+ def as_agent(self) -> AgentCallable:
29
+ """Return a callable compatible with Runtime.run_once."""
30
+
31
+
32
+ class PythonFunctionAdapter(FrameworkAdapter):
33
+ """Adapter for a plain Python function or coroutine.
34
+
35
+ This proves the framework-agnostic SDK path before adding heavier optional
36
+ integrations.
37
+ """
38
+
39
+ name = "python-function"
40
+
41
+ def __init__(self, func: AgentCallable, *, role: str = "Agent"):
42
+ self.func = func
43
+ self.role = role
44
+
45
+ def map_run_spec(self, framework_run: Any = None) -> dict[str, Any]:
46
+ return {"adapter": self.name, "role": self.role, "function": getattr(self.func, "__name__", repr(self.func))}
47
+
48
+ def as_agent(self) -> AgentCallable:
49
+ async def wrapped(ctx: AgentContext, state: dict[str, Any]) -> Any:
50
+ result = self.func(ctx, state)
51
+ if inspect.isawaitable(result):
52
+ return await result
53
+ return result
54
+
55
+ return wrapped
56
+
57
+
58
+ def python_agent(*, role: str = "Agent") -> Callable[[AgentCallable], PythonFunctionAdapter]:
59
+ def decorator(func: AgentCallable) -> PythonFunctionAdapter:
60
+ return PythonFunctionAdapter(func, role=role)
61
+
62
+ return decorator
@@ -0,0 +1,146 @@
1
+ from __future__ import annotations
2
+
3
+ import inspect
4
+ from typing import Any, Callable
5
+
6
+ from .adapters import AgentCallable, FrameworkAdapter
7
+ from .context import AgentContext
8
+
9
+ InputMapper = Callable[[AgentContext, dict[str, Any]], Any]
10
+
11
+
12
+ def default_input_mapper(_ctx: AgentContext, state: dict[str, Any]) -> dict[str, Any]:
13
+ return dict(state)
14
+
15
+
16
+ class MethodFrameworkAdapter(FrameworkAdapter):
17
+ """Dependency-free facade for framework objects with conventional methods."""
18
+
19
+ name = "method-framework"
20
+
21
+ def __init__(
22
+ self,
23
+ target: Any,
24
+ *,
25
+ role: str = "FrameworkAgent",
26
+ method_candidates: tuple[str, ...],
27
+ input_mapper: InputMapper | None = None,
28
+ output_key: str | None = "output",
29
+ ):
30
+ self.target = target
31
+ self.role = role
32
+ self.method_candidates = method_candidates
33
+ self.input_mapper = input_mapper or default_input_mapper
34
+ self.output_key = output_key
35
+
36
+ def map_run_spec(self, framework_run: Any = None) -> dict[str, Any]:
37
+ return {
38
+ "adapter": self.name,
39
+ "role": self.role,
40
+ "target": type(self.target).__name__,
41
+ "methods": list(self.method_candidates),
42
+ }
43
+
44
+ def as_agent(self) -> AgentCallable:
45
+ async def wrapped(ctx: AgentContext, state: dict[str, Any]) -> Any:
46
+ payload = self.input_mapper(ctx, state)
47
+ result = await self._invoke(payload)
48
+ if self.output_key is not None:
49
+ ctx.write_state_patch(self.output_key, result)
50
+ return result
51
+
52
+ return wrapped
53
+
54
+ async def _invoke(self, payload: Any) -> Any:
55
+ for name in self.method_candidates:
56
+ method = getattr(self.target, name, None)
57
+ if method is None:
58
+ continue
59
+ result = method(payload)
60
+ if inspect.isawaitable(result):
61
+ return await result
62
+ return result
63
+ if callable(self.target):
64
+ result = self.target(payload)
65
+ if inspect.isawaitable(result):
66
+ return await result
67
+ return result
68
+ raise AttributeError(f"{type(self.target).__name__} does not expose any of {self.method_candidates!r}")
69
+
70
+
71
+ class LangChainRunnableAdapter(MethodFrameworkAdapter):
72
+ """Wrap a LangChain-style Runnable without importing LangChain."""
73
+
74
+ name = "langchain-runnable"
75
+
76
+ def __init__(self, runnable: Any, *, role: str = "LangChainAgent", input_mapper: InputMapper | None = None, output_key: str | None = "langchain_output"):
77
+ super().__init__(runnable, role=role, method_candidates=("ainvoke", "invoke"), input_mapper=input_mapper, output_key=output_key)
78
+
79
+
80
+ class CrewAIAdapter(MethodFrameworkAdapter):
81
+ """Wrap a CrewAI-style Crew/Task object without importing CrewAI."""
82
+
83
+ name = "crewai"
84
+
85
+ def __init__(self, crew_or_task: Any, *, role: str = "CrewAIAgent", input_mapper: InputMapper | None = None, output_key: str | None = "crewai_output"):
86
+ super().__init__(crew_or_task, role=role, method_candidates=("akickoff", "kickoff", "arun", "run"), input_mapper=input_mapper, output_key=output_key)
87
+
88
+
89
+ class AutoGenAdapter(MethodFrameworkAdapter):
90
+ """Wrap an AutoGen-style agent object without importing AutoGen."""
91
+
92
+ name = "autogen"
93
+
94
+ def __init__(self, agent: Any, *, role: str = "AutoGenAgent", input_mapper: InputMapper | None = None, output_key: str | None = "autogen_output"):
95
+ super().__init__(
96
+ agent,
97
+ role=role,
98
+ method_candidates=("a_generate_reply", "generate_reply", "a_run", "run", "ainvoke", "invoke"),
99
+ input_mapper=input_mapper,
100
+ output_key=output_key,
101
+ )
102
+
103
+
104
+ class OpenAIAgentsSDKAdapter(MethodFrameworkAdapter):
105
+ """Wrap an OpenAI Agents SDK-style runner without importing the SDK."""
106
+
107
+ name = "openai-agents-sdk"
108
+
109
+ def __init__(self, agent_or_runner: Any, *, role: str = "OpenAIAgent", input_mapper: InputMapper | None = None, output_key: str | None = "openai_agent_output"):
110
+ super().__init__(
111
+ agent_or_runner,
112
+ role=role,
113
+ method_candidates=("arun", "run", "ainvoke", "invoke"),
114
+ input_mapper=input_mapper,
115
+ output_key=output_key,
116
+ )
117
+
118
+
119
+ class LlamaIndexAdapter(MethodFrameworkAdapter):
120
+ """Wrap a LlamaIndex-style query/chat/retriever object without importing LlamaIndex."""
121
+
122
+ name = "llamaindex"
123
+
124
+ def __init__(self, query_engine_or_agent: Any, *, role: str = "LlamaIndexAgent", input_mapper: InputMapper | None = None, output_key: str | None = "llamaindex_output"):
125
+ super().__init__(
126
+ query_engine_or_agent,
127
+ role=role,
128
+ method_candidates=("aquery", "query", "achat", "chat", "aretrieve", "retrieve", "ainvoke", "invoke"),
129
+ input_mapper=input_mapper,
130
+ output_key=output_key,
131
+ )
132
+
133
+
134
+ class SemanticKernelAdapter(MethodFrameworkAdapter):
135
+ """Wrap a Semantic Kernel-style kernel/function object without importing it."""
136
+
137
+ name = "semantic-kernel"
138
+
139
+ def __init__(self, kernel_or_function: Any, *, role: str = "SemanticKernelAgent", input_mapper: InputMapper | None = None, output_key: str | None = "semantic_kernel_output"):
140
+ super().__init__(
141
+ kernel_or_function,
142
+ role=role,
143
+ method_candidates=("ainvoke", "invoke", "invoke_prompt", "run_async", "run"),
144
+ input_mapper=input_mapper,
145
+ output_key=output_key,
146
+ )
@@ -0,0 +1,140 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Any
4
+
5
+ from .adapters import FrameworkAdapter, PythonFunctionAdapter
6
+ from .ids import new_id
7
+
8
+
9
+ class LangGraphCheckpointerAdapter:
10
+ """Dependency-free LangGraph-style checkpointer adapter.
11
+
12
+ Runtime core does not import LangGraph. This adapter exposes the common
13
+ checkpointer shape (`put`, `get`, `get_tuple`, `list`, `put_writes`) using
14
+ plain dictionaries so optional packages can wrap it with LangGraph's exact
15
+ classes without changing AgentLedger state semantics.
16
+ """
17
+
18
+ name = "langgraph-checkpointer"
19
+
20
+ def __init__(self, runtime: Any):
21
+ self.runtime = runtime
22
+
23
+ def config_for_run(self, run_id: str, *, thread_id: str | None = None, checkpoint_ns: str = "") -> dict[str, Any]:
24
+ return {"configurable": {"agentledger_run_id": run_id, "thread_id": thread_id or run_id, "checkpoint_ns": checkpoint_ns}}
25
+
26
+ def checkpoint_from_run(self, run_id: str) -> dict[str, Any]:
27
+ state, state_version, session_id = self.runtime.store.load_state(run_id)
28
+ return {"run_id": run_id, "session_id": session_id, "state_version": state_version, "state": state}
29
+
30
+ def persist_checkpoint(self, run_id: str, checkpoint: dict[str, Any], *, reason: str = "langgraph checkpoint") -> int:
31
+ return self.runtime.store.apply_system_state_patch(run_id=run_id, patch={"langgraph_checkpoint": checkpoint}, reason=reason)
32
+
33
+ def put(
34
+ self,
35
+ config: dict[str, Any],
36
+ checkpoint: dict[str, Any],
37
+ metadata: dict[str, Any] | None = None,
38
+ new_versions: dict[str, Any] | None = None,
39
+ ) -> dict[str, Any]:
40
+ run_id = self._run_id_from_config(config)
41
+ checkpoint_id = str(checkpoint.get("id") or checkpoint.get("checkpoint_id") or new_id("lgckpt"))
42
+ next_config = self._with_checkpoint_id(config, checkpoint_id)
43
+ record = {
44
+ "checkpoint": {**checkpoint, "id": checkpoint_id},
45
+ "metadata": metadata or {},
46
+ "new_versions": new_versions or {},
47
+ "config": next_config,
48
+ }
49
+ self.runtime.store.apply_system_state_patch(
50
+ run_id=run_id,
51
+ patch={"langgraph_checkpoint": record, "langgraph_pending_writes": []},
52
+ reason="langgraph checkpoint put",
53
+ )
54
+ return next_config
55
+
56
+ async def aput(
57
+ self,
58
+ config: dict[str, Any],
59
+ checkpoint: dict[str, Any],
60
+ metadata: dict[str, Any] | None = None,
61
+ new_versions: dict[str, Any] | None = None,
62
+ ) -> dict[str, Any]:
63
+ return self.put(config, checkpoint, metadata, new_versions)
64
+
65
+ def get_tuple(self, config: dict[str, Any]) -> dict[str, Any] | None:
66
+ run_id = self._run_id_from_config(config)
67
+ state = self.runtime.store.final_state(run_id)
68
+ record = state.get("langgraph_checkpoint")
69
+ if record is None:
70
+ return None
71
+ if "checkpoint" not in record:
72
+ record = {"checkpoint": record, "metadata": {}, "config": config}
73
+ return {
74
+ "config": record.get("config", config),
75
+ "checkpoint": record.get("checkpoint"),
76
+ "metadata": record.get("metadata", {}),
77
+ "parent_config": record.get("parent_config"),
78
+ "pending_writes": state.get("langgraph_pending_writes", []),
79
+ }
80
+
81
+ async def aget_tuple(self, config: dict[str, Any]) -> dict[str, Any] | None:
82
+ return self.get_tuple(config)
83
+
84
+ def get(self, config: dict[str, Any]) -> dict[str, Any] | None:
85
+ item = self.get_tuple(config)
86
+ return item["checkpoint"] if item is not None else None
87
+
88
+ async def aget(self, config: dict[str, Any]) -> dict[str, Any] | None:
89
+ return self.get(config)
90
+
91
+ def list(self, config: dict[str, Any] | None = None, **_kwargs: Any) -> list[dict[str, Any]]:
92
+ if config is None:
93
+ return []
94
+ item = self.get_tuple(config)
95
+ return [item] if item is not None else []
96
+
97
+ async def alist(self, config: dict[str, Any] | None = None, **kwargs: Any) -> list[dict[str, Any]]:
98
+ return self.list(config, **kwargs)
99
+
100
+ def put_writes(self, config: dict[str, Any], writes: list[Any], task_id: str, task_path: str = "") -> None:
101
+ run_id = self._run_id_from_config(config)
102
+ state = self.runtime.store.final_state(run_id)
103
+ pending = list(state.get("langgraph_pending_writes", []))
104
+ pending.append({"task_id": task_id, "task_path": task_path, "writes": writes, "config": config})
105
+ self.runtime.store.apply_system_state_patch(
106
+ run_id=run_id,
107
+ patch={"langgraph_pending_writes": pending},
108
+ reason="langgraph pending writes",
109
+ )
110
+
111
+ async def aput_writes(self, config: dict[str, Any], writes: list[Any], task_id: str, task_path: str = "") -> None:
112
+ self.put_writes(config, writes, task_id, task_path)
113
+
114
+ def _run_id_from_config(self, config: dict[str, Any]) -> str:
115
+ configurable = config.get("configurable", {}) if isinstance(config, dict) else {}
116
+ run_id = configurable.get("agentledger_run_id") or configurable.get("run_id")
117
+ if not run_id:
118
+ raise ValueError("LangGraph config must include configurable.agentledger_run_id or configurable.run_id")
119
+ return str(run_id)
120
+
121
+ def _with_checkpoint_id(self, config: dict[str, Any], checkpoint_id: str) -> dict[str, Any]:
122
+ configurable = dict(config.get("configurable", {}))
123
+ configurable["checkpoint_id"] = checkpoint_id
124
+ return {**config, "configurable": configurable}
125
+
126
+
127
+ class LangGraphNodeAdapter(FrameworkAdapter):
128
+ """Wrap a callable node as a Runtime.run_once-compatible agent."""
129
+
130
+ name = "langgraph-node"
131
+
132
+ def __init__(self, node: Any, *, role: str = "LangGraphAgent"):
133
+ self.node = node
134
+ self.role = role
135
+
136
+ def map_run_spec(self, framework_run: Any = None) -> dict[str, Any]:
137
+ return {"adapter": self.name, "role": self.role, "node": getattr(self.node, "__name__", repr(self.node))}
138
+
139
+ def as_agent(self):
140
+ return PythonFunctionAdapter(self.node, role=self.role).as_agent()
@@ -0,0 +1,145 @@
1
+ from __future__ import annotations
2
+
3
+ import inspect
4
+ from dataclasses import dataclass
5
+ from typing import Any, Callable
6
+
7
+ from .tools import ToolRegistry, ToolSpec
8
+
9
+ MCPCall = Callable[[str, dict[str, Any]], Any]
10
+ MCPResourceRead = Callable[[str], Any]
11
+
12
+
13
+ @dataclass(frozen=True)
14
+ class MCPResourceDescriptor:
15
+ uri: str
16
+ name: str
17
+ mime_type: str = "application/json"
18
+
19
+ def to_dict(self) -> dict[str, Any]:
20
+ return {"uri": self.uri, "name": self.name, "mimeType": self.mime_type}
21
+
22
+
23
+ class InMemoryMCPToolServer:
24
+ """Dependency-free MCP-style tool server fixture for examples/tests."""
25
+
26
+ def __init__(self) -> None:
27
+ self._tools: dict[str, tuple[dict[str, Any], MCPCall]] = {}
28
+
29
+ def add_tool(self, descriptor: dict[str, Any], handler: MCPCall) -> None:
30
+ self._tools[descriptor["name"]] = (descriptor, handler)
31
+
32
+ def list_tools(self) -> list[dict[str, Any]]:
33
+ return [self._tools[name][0] for name in sorted(self._tools)]
34
+
35
+ def call_tool(self, name: str, args: dict[str, Any]) -> Any:
36
+ try:
37
+ _descriptor, handler = self._tools[name]
38
+ except KeyError as exc:
39
+ raise KeyError(f"MCP tool not found: {name}") from exc
40
+ return handler(name, args)
41
+
42
+
43
+ class InMemoryMCPContextServer:
44
+ """Dependency-free MCP-style context/resource server fixture."""
45
+
46
+ def __init__(self) -> None:
47
+ self._resources: dict[str, tuple[MCPResourceDescriptor, MCPResourceRead]] = {}
48
+
49
+ def add_resource(self, *, uri: str, name: str, reader: MCPResourceRead, mime_type: str = "application/json") -> None:
50
+ self._resources[uri] = (MCPResourceDescriptor(uri=uri, name=name, mime_type=mime_type), reader)
51
+
52
+ def list_resources(self) -> list[dict[str, Any]]:
53
+ return [self._resources[uri][0].to_dict() for uri in sorted(self._resources)]
54
+
55
+ def read_resource(self, uri: str) -> Any:
56
+ try:
57
+ descriptor, reader = self._resources[uri]
58
+ except KeyError as exc:
59
+ raise KeyError(f"MCP resource not found: {uri}") from exc
60
+ return {"resource": descriptor.to_dict(), "content": reader(uri)}
61
+
62
+
63
+ class MCPToolAdapter:
64
+ """Map MCP-style tool descriptors into AgentLedger ToolSpec objects.
65
+
66
+ The adapter is dependency-free: callers provide a `client_call` function that
67
+ knows how to invoke an MCP client. Runtime core still owns policy, ledger,
68
+ audit, budget, replay, and shadow semantics through ToolGateway.
69
+ """
70
+
71
+ name = "mcp-tool"
72
+
73
+ def __init__(self, client_call: MCPCall):
74
+ self.client_call = client_call
75
+
76
+ def tool_spec_from_descriptor(self, descriptor: dict[str, Any]) -> ToolSpec:
77
+ tool_name = descriptor["name"]
78
+ annotations = descriptor.get("annotations", {}) or {}
79
+ input_schema = descriptor.get("inputSchema") or descriptor.get("input_schema") or {}
80
+ side_effect = annotations.get("side_effect", "none")
81
+ risk_level = annotations.get("risk_level", "low")
82
+ idempotency_required = bool(annotations.get("idempotency_required", side_effect != "none"))
83
+
84
+ async def call(args: dict[str, Any]) -> Any:
85
+ result = self.client_call(tool_name, args)
86
+ if inspect.isawaitable(result):
87
+ return await result
88
+ return result
89
+
90
+ return ToolSpec(
91
+ name=tool_name,
92
+ func=call,
93
+ version=str(descriptor.get("version", "v1")),
94
+ input_schema=input_schema,
95
+ output_schema=descriptor.get("outputSchema") or descriptor.get("output_schema") or {},
96
+ side_effect=side_effect,
97
+ risk_level=risk_level,
98
+ idempotency_required=idempotency_required,
99
+ )
100
+
101
+ def register(self, registry: ToolRegistry, descriptor: dict[str, Any]) -> ToolSpec:
102
+ spec = self.tool_spec_from_descriptor(descriptor)
103
+ registry.register(spec)
104
+ return spec
105
+
106
+ def register_all(self, registry: ToolRegistry, descriptors: list[dict[str, Any]]) -> list[ToolSpec]:
107
+ return [self.register(registry, descriptor) for descriptor in descriptors]
108
+
109
+
110
+ class MCPContextAdapter:
111
+ """Expose MCP-style context/resource reads through ToolGateway."""
112
+
113
+ name = "mcp-context"
114
+
115
+ def __init__(self, resource_read: Callable[[str], Any]):
116
+ self.resource_read = resource_read
117
+
118
+ def read_tool_spec(self, *, name: str = "mcp.context.read", risk_level: str = "low") -> ToolSpec:
119
+ async def call(args: dict[str, Any]) -> Any:
120
+ uri = args["uri"]
121
+ result = self.resource_read(uri)
122
+ if inspect.isawaitable(result):
123
+ return await result
124
+ return result
125
+
126
+ return ToolSpec(
127
+ name=name,
128
+ func=call,
129
+ version="v1",
130
+ description="Read an MCP-style context resource by URI.",
131
+ input_schema={
132
+ "type": "object",
133
+ "required": ["uri"],
134
+ "properties": {"uri": {"type": "string", "minLength": 1}},
135
+ "additionalProperties": False,
136
+ },
137
+ output_schema={"type": "object"},
138
+ side_effect="none",
139
+ risk_level=risk_level,
140
+ )
141
+
142
+ def register_read_tool(self, registry: ToolRegistry, *, name: str = "mcp.context.read", risk_level: str = "low") -> ToolSpec:
143
+ spec = self.read_tool_spec(name=name, risk_level=risk_level)
144
+ registry.register(spec)
145
+ return spec