adapt-agent 0.2.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 (64) hide show
  1. adapt_agent/__init__.py +100 -0
  2. adapt_agent/__main__.py +8 -0
  3. adapt_agent/adapters/__init__.py +47 -0
  4. adapt_agent/adapters/_governed.py +496 -0
  5. adapt_agent/adapters/base.py +79 -0
  6. adapt_agent/adapters/claude_agent/__init__.py +49 -0
  7. adapt_agent/adapters/crewai/__init__.py +71 -0
  8. adapt_agent/adapters/google_adk/__init__.py +50 -0
  9. adapt_agent/adapters/langgraph/__init__.py +45 -0
  10. adapt_agent/adapters/microsoft_agent_framework/__init__.py +47 -0
  11. adapt_agent/adapters/openai_agents/__init__.py +65 -0
  12. adapt_agent/adapters/pydantic_ai/__init__.py +45 -0
  13. adapt_agent/adversarial/__init__.py +254 -0
  14. adapt_agent/cli/__init__.py +885 -0
  15. adapt_agent/cli/__main__.py +8 -0
  16. adapt_agent/core/__init__.py +13 -0
  17. adapt_agent/core/memory.py +223 -0
  18. adapt_agent/core/middleware.py +288 -0
  19. adapt_agent/core/policy.py +404 -0
  20. adapt_agent/core/trust.py +200 -0
  21. adapt_agent/core/types.py +82 -0
  22. adapt_agent/evaluation/__init__.py +209 -0
  23. adapt_agent/exceptions.py +52 -0
  24. adapt_agent/observability/__init__.py +287 -0
  25. adapt_agent/optimization/__init__.py +191 -0
  26. adapt_agent/optimization/config.py +701 -0
  27. adapt_agent/optimization/dataset.py +271 -0
  28. adapt_agent/optimization/evals.py +257 -0
  29. adapt_agent/optimization/evaluation.py +329 -0
  30. adapt_agent/optimization/extractors.py +346 -0
  31. adapt_agent/optimization/introspection/__init__.py +305 -0
  32. adapt_agent/optimization/introspection/claude_agent.py +150 -0
  33. adapt_agent/optimization/introspection/crewai.py +190 -0
  34. adapt_agent/optimization/introspection/google_adk.py +218 -0
  35. adapt_agent/optimization/introspection/langgraph.py +245 -0
  36. adapt_agent/optimization/introspection/microsoft_agent_framework.py +177 -0
  37. adapt_agent/optimization/introspection/openai_agents.py +219 -0
  38. adapt_agent/optimization/introspection/pydantic_ai.py +238 -0
  39. adapt_agent/optimization/judge.py +771 -0
  40. adapt_agent/optimization/judges.py +254 -0
  41. adapt_agent/optimization/metrics.py +424 -0
  42. adapt_agent/optimization/optimizers.py +815 -0
  43. adapt_agent/optimization/parameters.py +307 -0
  44. adapt_agent/optimization/performance.py +201 -0
  45. adapt_agent/optimization/proposers.py +464 -0
  46. adapt_agent/optimization/providers.py +783 -0
  47. adapt_agent/optimization/runners.py +244 -0
  48. adapt_agent/optimization/target.py +318 -0
  49. adapt_agent/patches/__init__.py +114 -0
  50. adapt_agent/py.typed +0 -0
  51. adapt_agent/security/__init__.py +11 -0
  52. adapt_agent/security/firewall.py +366 -0
  53. adapt_agent/security/taint_tracker.py +346 -0
  54. adapt_agent/skills/__init__.py +460 -0
  55. adapt_agent/skills/adapt-agent/SKILL.md +232 -0
  56. adapt_agent/skills/adapt-agent/references/evals.md +248 -0
  57. adapt_agent/skills/adapt-agent/references/guardrails.md +313 -0
  58. adapt_agent/skills/adapt-agent/references/optimization.md +227 -0
  59. adapt_agent-0.2.0.dist-info/METADATA +401 -0
  60. adapt_agent-0.2.0.dist-info/RECORD +64 -0
  61. adapt_agent-0.2.0.dist-info/WHEEL +5 -0
  62. adapt_agent-0.2.0.dist-info/entry_points.txt +3 -0
  63. adapt_agent-0.2.0.dist-info/licenses/LICENSE +21 -0
  64. adapt_agent-0.2.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,100 @@
1
+ """
2
+ ADAPT-Agent: Adversarial Defense & Policy Training for LLM Agents
3
+
4
+ A comprehensive library for LLM agent optimization and security.
5
+ """
6
+
7
+ __version__ = "0.2.0"
8
+
9
+ # Core exports
10
+ # Framework adapters
11
+ from adapt_agent.adapters import BaseAdapter
12
+
13
+ # Adversarial defense
14
+ from adapt_agent.adversarial import AdversarialDefense
15
+ from adapt_agent.core import (
16
+ MemorySystem,
17
+ Middleware,
18
+ PolicyEnforcer,
19
+ TrustManager,
20
+ )
21
+ from adapt_agent.core.types import (
22
+ Adapter,
23
+ Agent,
24
+ AgentMessage,
25
+ AgentState,
26
+ PolicyRule,
27
+ SecurityEvent,
28
+ TrustScore,
29
+ )
30
+
31
+ # Evaluation, observability, optimization
32
+ from adapt_agent.evaluation import AgentEvaluator
33
+ from adapt_agent.observability import AgentObserver
34
+ from adapt_agent.optimization import (
35
+ AgentOptimizer,
36
+ CoordinateAscentOptimizer,
37
+ EvaluationHarness,
38
+ Example,
39
+ GoldenDataset,
40
+ LLMJudge,
41
+ ModelProvider,
42
+ OptimizableAgent,
43
+ Parameter,
44
+ ParameterKind,
45
+ get_provider,
46
+ make_default_optimizer,
47
+ )
48
+
49
+ # Patches
50
+ from adapt_agent.patches import PatchManager
51
+
52
+ # Security exports
53
+ from adapt_agent.security import (
54
+ Firewall,
55
+ TaintLevel,
56
+ TaintSource,
57
+ TaintTracker,
58
+ )
59
+
60
+ __all__ = [
61
+ "__version__",
62
+ # Core
63
+ "TrustManager",
64
+ "PolicyEnforcer",
65
+ "MemorySystem",
66
+ "Middleware",
67
+ # Types
68
+ "Agent",
69
+ "Adapter",
70
+ "AgentMessage",
71
+ "AgentState",
72
+ "TrustScore",
73
+ "PolicyRule",
74
+ "SecurityEvent",
75
+ # Security
76
+ "Firewall",
77
+ "TaintTracker",
78
+ "TaintLevel",
79
+ "TaintSource",
80
+ # Adversarial / evaluation / observability / optimization
81
+ "AdversarialDefense",
82
+ "AgentEvaluator",
83
+ "AgentObserver",
84
+ "AgentOptimizer",
85
+ # Dataset-driven optimization & evaluation
86
+ "GoldenDataset",
87
+ "Example",
88
+ "EvaluationHarness",
89
+ "LLMJudge",
90
+ "ModelProvider",
91
+ "get_provider",
92
+ "OptimizableAgent",
93
+ "Parameter",
94
+ "ParameterKind",
95
+ "CoordinateAscentOptimizer",
96
+ "make_default_optimizer",
97
+ # Patches & adapters
98
+ "PatchManager",
99
+ "BaseAdapter",
100
+ ]
@@ -0,0 +1,8 @@
1
+ """Enable ``python -m adapt_agent`` to invoke the CLI."""
2
+
3
+ import sys
4
+
5
+ from adapt_agent.cli import main
6
+
7
+ if __name__ == "__main__":
8
+ sys.exit(main())
@@ -0,0 +1,47 @@
1
+ """Framework adapters for ADAPT-Agent.
2
+
3
+ Adapters integrate ADAPT-Agent's security and governance primitives with
4
+ third-party LLM agent frameworks. Every adapter applies the same pipeline on
5
+ each ``execute`` call -- input screening, policy enforcement, middleware, traced
6
+ execution, output screening -- regardless of the underlying framework.
7
+
8
+ Support matrix:
9
+
10
+ ========================== ========== =========================================
11
+ Framework Status Class
12
+ ========================== ========== =========================================
13
+ LangGraph Supported :class:`LangGraphAdapter`
14
+ Microsoft Agent Framework Supported :class:`MicrosoftAgentFrameworkAdapter`
15
+ Google ADK Supported :class:`GoogleADKAdapter`
16
+ Pydantic AI Supported :class:`PydanticAIAdapter`
17
+ CrewAI Supported :class:`CrewAIAdapter`
18
+ OpenAI Agents SDK Supported :class:`OpenAIAgentsAdapter`
19
+ Claude Agent SDK Supported :class:`ClaudeAgentSDKAdapter`
20
+ ========================== ========== =========================================
21
+
22
+ Importing an adapter class never imports the underlying framework; the framework
23
+ is only imported lazily when you actually wrap and run an agent, so
24
+ ``adapt_agent`` stays import-safe without optional dependencies installed.
25
+ """
26
+
27
+ from adapt_agent.adapters._governed import GovernedAdapter
28
+ from adapt_agent.adapters.base import BaseAdapter
29
+ from adapt_agent.adapters.claude_agent import ClaudeAgentSDKAdapter
30
+ from adapt_agent.adapters.crewai import CrewAIAdapter
31
+ from adapt_agent.adapters.google_adk import GoogleADKAdapter
32
+ from adapt_agent.adapters.langgraph import LangGraphAdapter
33
+ from adapt_agent.adapters.microsoft_agent_framework import MicrosoftAgentFrameworkAdapter
34
+ from adapt_agent.adapters.openai_agents import OpenAIAgentsAdapter
35
+ from adapt_agent.adapters.pydantic_ai import PydanticAIAdapter
36
+
37
+ __all__ = [
38
+ "BaseAdapter",
39
+ "GovernedAdapter",
40
+ "LangGraphAdapter",
41
+ "MicrosoftAgentFrameworkAdapter",
42
+ "GoogleADKAdapter",
43
+ "PydanticAIAdapter",
44
+ "CrewAIAdapter",
45
+ "OpenAIAgentsAdapter",
46
+ "ClaudeAgentSDKAdapter",
47
+ ]
@@ -0,0 +1,496 @@
1
+ """Shared governance machinery for ADAPT-Agent framework adapters.
2
+
3
+ This module factors out the framework-agnostic parts of an adapter so that each
4
+ concrete adapter (LangGraph, CrewAI, Pydantic AI, ...) only has to declare a few
5
+ small details: which method runs the underlying agent, how to shape the input,
6
+ and a human-readable framework name.
7
+
8
+ The :class:`GovernedAdapter` applies, in order, on every ``execute`` call:
9
+
10
+ 1. **Input screening** -- :class:`~adapt_agent.security.Firewall` and
11
+ :class:`~adapt_agent.adversarial.AdversarialDefense` scan every string found
12
+ in the payload.
13
+ 2. **Policy enforcement** -- a :class:`~adapt_agent.core.PolicyEnforcer` is
14
+ evaluated against the extracted :class:`~adapt_agent.core.types.AgentState`.
15
+ 3. **Pre-middleware** -- a :class:`~adapt_agent.core.Middleware` pipeline may
16
+ rewrite the payload.
17
+ 4. **Traced execution** -- the framework agent runs, optionally traced by an
18
+ :class:`~adapt_agent.observability.AgentObserver`.
19
+ 5. **Post-middleware** -- the pipeline may rewrite the result.
20
+ 6. **Output screening** -- the firewall scans every string in the result.
21
+
22
+ The design is intentionally *structural*: importing an adapter never imports the
23
+ underlying framework. The framework is only needed at runtime to build the agent
24
+ you pass to :meth:`wrap_agent`. Adapters wrap anything that exposes a recognized
25
+ "run" method -- or any plain callable -- so they work without the optional
26
+ dependency installed and are trivial to unit-test.
27
+
28
+ Async support
29
+ -------------
30
+ Several modern frameworks expose async-only entry points. ``execute`` therefore
31
+ resolves the framework result transparently:
32
+
33
+ * a coroutine is run to completion;
34
+ * an async generator (or any async iterator) is drained into a list;
35
+ * a sync generator is likewise materialised into a list.
36
+
37
+ When called from inside a running event loop, blocking on a coroutine is not
38
+ possible; in that case a clear :class:`AdapterError` is raised pointing the
39
+ caller at the framework's native async API.
40
+ """
41
+
42
+ import asyncio
43
+ import inspect
44
+ import uuid
45
+ from collections.abc import AsyncIterable, Callable
46
+ from typing import Any, cast
47
+
48
+ from adapt_agent.adapters.base import BaseAdapter
49
+ from adapt_agent.adversarial import AdversarialDefense
50
+ from adapt_agent.core.middleware import Middleware
51
+ from adapt_agent.core.policy import PolicyEnforcer
52
+ from adapt_agent.core.types import Agent, AgentState
53
+ from adapt_agent.exceptions import AdapterError, SecurityBlockedError
54
+ from adapt_agent.observability import AgentObserver
55
+ from adapt_agent.security.firewall import Firewall
56
+
57
+ #: A callable that runs a framework agent given its prepared input. Accepts
58
+ #: arbitrary call signatures (some adapters call it with keyword arguments).
59
+ Runner = Callable[..., Any]
60
+
61
+ #: Attribute names that commonly hold human-readable text on framework result
62
+ #: and message objects (Pydantic AI ``.output``, Microsoft Agent Framework
63
+ #: ``.text``, CrewAI ``.raw``, OpenAI Agents ``.final_output``, Claude
64
+ #: ``ResultMessage.result`` / ``TextBlock.text``, LangChain ``.content`` ...).
65
+ _TEXT_ATTRS = (
66
+ "content",
67
+ "text",
68
+ "output",
69
+ "final_output",
70
+ "raw",
71
+ "result",
72
+ "data",
73
+ )
74
+
75
+ #: Attribute names that hold *containers* of further text-bearing objects
76
+ #: (e.g. ``Content.parts`` in Google ADK, message lists, CrewAI ``tasks_output``).
77
+ #: These are recursed into even when they are framework objects rather than
78
+ #: plain dicts/lists, so screening reaches deeply-structured results.
79
+ _RECURSE_ATTRS = (
80
+ "content",
81
+ "parts",
82
+ "messages",
83
+ "tasks_output",
84
+ )
85
+
86
+
87
+ def _safe_getattr(obj: Any, attr: str) -> Any:
88
+ """``getattr(obj, attr, None)`` that never propagates.
89
+
90
+ Framework result objects may expose attributes via descriptors/properties
91
+ that raise. Since text extraction feeds the security pipeline, a raising
92
+ attribute must not crash the whole execution -- treat it as absent.
93
+ """
94
+ try:
95
+ return getattr(obj, attr, None)
96
+ except Exception:
97
+ return None
98
+
99
+
100
+ def _extract_texts(data: Any) -> list[str]:
101
+ """Best-effort extraction of human-readable text from an arbitrary payload.
102
+
103
+ Adapter payloads are typically dicts that may contain a ``messages`` list or
104
+ arbitrary string fields, but framework result objects expose their text via
105
+ attributes instead (see :data:`_TEXT_ATTRS`). We collect every string we can
106
+ reach so the security controls can scan it without assuming a fixed schema.
107
+ """
108
+ texts: list[str] = []
109
+
110
+ def _walk(value: Any, depth: int = 0) -> None:
111
+ if depth > 6: # bound recursion (defensive against pathological nesting)
112
+ return
113
+ if isinstance(value, str):
114
+ texts.append(value)
115
+ elif value is None or isinstance(value, (int, float, bool)):
116
+ # Primitives carry no text and have no attributes worth probing;
117
+ # returning early avoids pointless getattr lookups on large payloads.
118
+ return
119
+ elif isinstance(value, dict):
120
+ for v in value.values():
121
+ _walk(v, depth + 1)
122
+ elif isinstance(value, (list, tuple)):
123
+ for v in value:
124
+ _walk(v, depth + 1)
125
+ else:
126
+ # Framework message / result objects expose their text via one of a
127
+ # handful of well-known attributes. Recurse into anything walkable.
128
+ for attr in _TEXT_ATTRS:
129
+ inner = _safe_getattr(value, attr)
130
+ if isinstance(inner, str):
131
+ texts.append(inner)
132
+ elif isinstance(inner, (dict, list, tuple)):
133
+ _walk(inner, depth + 1)
134
+ # Structured content containers (e.g. genai ``Content.parts``) hold
135
+ # further objects whose text we still want to scan.
136
+ for attr in _RECURSE_ATTRS:
137
+ inner = _safe_getattr(value, attr)
138
+ if inner is not None and not isinstance(inner, (str, int, float, bool)):
139
+ _walk(inner, depth + 1)
140
+
141
+ _walk(data)
142
+ return texts
143
+
144
+
145
+ def _extract_prompt(payload: Any) -> str:
146
+ """Derive a single prompt string from an adapter payload.
147
+
148
+ Many frameworks (Pydantic AI, OpenAI Agents, Microsoft Agent Framework,
149
+ Claude Agent SDK) run from a string prompt rather than a state dict. This
150
+ helper picks the most plausible prompt: the latest user message, then a
151
+ common prompt-like key, then a string fallback. It accepts a state dict, a
152
+ bare list/tuple of messages, or a plain string.
153
+ """
154
+ if isinstance(payload, str):
155
+ return payload
156
+
157
+ if isinstance(payload, dict):
158
+ messages: Any = payload.get("messages")
159
+ elif isinstance(payload, (list, tuple)):
160
+ messages = payload
161
+ else:
162
+ messages = None
163
+
164
+ if isinstance(messages, (list, tuple)) and messages:
165
+ for message in reversed(messages):
166
+ if isinstance(message, dict):
167
+ role, content = message.get("role"), message.get("content")
168
+ else:
169
+ role = _safe_getattr(message, "role")
170
+ content = _safe_getattr(message, "content")
171
+ role_str = role.lower() if isinstance(role, str) else None
172
+ if isinstance(content, str) and (role_str == "user" or role is None):
173
+ return content
174
+ last = messages[-1]
175
+ last_content = (
176
+ last.get("content") if isinstance(last, dict) else _safe_getattr(last, "content")
177
+ )
178
+ if isinstance(last_content, str):
179
+ return last_content
180
+
181
+ if isinstance(payload, dict):
182
+ for key in ("prompt", "input", "query", "text"):
183
+ value = payload.get(key)
184
+ if isinstance(value, str):
185
+ return value
186
+ return str(payload)
187
+
188
+
189
+ def _resolve_result(value: Any) -> Any:
190
+ """Materialise a sync/async framework result into a concrete value.
191
+
192
+ * Coroutines are run to completion.
193
+ * Async generators / async iterators are drained into a list.
194
+ * Sync generators are materialised into a list.
195
+
196
+ Raises:
197
+ AdapterError: If an awaitable is produced while an event loop is already
198
+ running on this thread (where blocking is unsafe).
199
+ """
200
+ # ``AsyncIterable`` covers both ``async def`` generators and custom async
201
+ # iterators (``__aiter__``/``__anext__``), which streaming SDKs often return.
202
+ if isinstance(value, AsyncIterable):
203
+ return _run_coro(_drain_async_gen(value))
204
+ if inspect.isawaitable(value):
205
+ return _run_coro(_await(value))
206
+ if inspect.isgenerator(value):
207
+ return list(value)
208
+ return value
209
+
210
+
211
+ async def _await(value: Any) -> Any:
212
+ return await value
213
+
214
+
215
+ async def _drain_async_gen(gen: Any) -> list[Any]:
216
+ return [item async for item in gen]
217
+
218
+
219
+ def _run_coro(coro: Any) -> Any:
220
+ try:
221
+ running = asyncio.get_running_loop()
222
+ except RuntimeError:
223
+ running = None
224
+ if running is not None:
225
+ coro.close()
226
+ raise AdapterError(
227
+ "Cannot synchronously run an async agent from inside a running event "
228
+ "loop. Call the framework's native async API directly, or run "
229
+ "execute() in a worker thread."
230
+ )
231
+ return asyncio.run(coro)
232
+
233
+
234
+ class GovernedAdapter(BaseAdapter):
235
+ """Base adapter that wraps a framework agent with ADAPT-Agent governance.
236
+
237
+ Subclasses customise behaviour by overriding the class attributes below and,
238
+ where a framework needs special handling, the :meth:`_resolve_runner`,
239
+ :meth:`_prepare_input` and :meth:`_call_runner` hooks.
240
+
241
+ Class attributes:
242
+ framework_name: Human-readable framework name (used by
243
+ :meth:`get_framework_name` and default ``agent_id``).
244
+ run_method_names: Method names to look for on the wrapped object, in
245
+ priority order. The first callable match becomes the runner. If none
246
+ match and the object itself is callable, the object is used directly.
247
+ operation: Trace operation label recorded by the observer.
248
+
249
+ Args:
250
+ config: Optional adapter configuration dictionary.
251
+ firewall: Optional :class:`Firewall` used to screen inputs/outputs.
252
+ defense: Optional :class:`AdversarialDefense` used on inputs.
253
+ policy_enforcer: Optional :class:`PolicyEnforcer` evaluated against the
254
+ extracted agent state before execution.
255
+ observer: Optional :class:`AgentObserver` used to trace executions.
256
+ middleware: Optional :class:`Middleware` pipeline applied to the input
257
+ payload (pre) and the result payload (post).
258
+ agent_id: Stable identifier used in traces and policy checks. Defaults to
259
+ ``"<framework>-agent"``.
260
+ block_on_violation: When ``True`` (default), a firewall/defense hit or a
261
+ ``block`` policy action raises :class:`SecurityBlockedError`. When
262
+ ``False`` the execution proceeds but threats are still recorded.
263
+ """
264
+
265
+ framework_name: str = "Governed"
266
+ run_method_names: tuple[str, ...] = ("invoke",)
267
+ operation: str = "invoke"
268
+
269
+ def __init__(
270
+ self,
271
+ config: dict[str, Any] | None = None,
272
+ *,
273
+ firewall: Firewall | None = None,
274
+ defense: AdversarialDefense | None = None,
275
+ policy_enforcer: PolicyEnforcer | None = None,
276
+ observer: AgentObserver | None = None,
277
+ middleware: Middleware | None = None,
278
+ agent_id: str | None = None,
279
+ block_on_violation: bool = True,
280
+ ):
281
+ super().__init__(config)
282
+ self.firewall = firewall
283
+ self.defense = defense
284
+ self.policy_enforcer = policy_enforcer
285
+ self.observer = observer
286
+ self.middleware = middleware
287
+ self.agent_id = agent_id or f"{self.framework_name.lower().replace(' ', '-')}-agent"
288
+ self.block_on_violation = block_on_violation
289
+
290
+ # -- framework-specific hooks ---------------------------------------------
291
+
292
+ def _resolve_runner(self, agent: Any) -> Runner | None:
293
+ """Return a callable that runs ``agent`` given prepared input, or ``None``.
294
+
295
+ The default looks for the first callable attribute named in
296
+ :attr:`run_method_names`, then falls back to the object itself if it is
297
+ callable. Subclasses may override to support runner/agent frameworks.
298
+ """
299
+ for name in self.run_method_names:
300
+ candidate = getattr(agent, name, None)
301
+ if callable(candidate):
302
+ return cast(Runner, candidate)
303
+ if callable(agent):
304
+ return cast(Runner, agent)
305
+ return None
306
+
307
+ def _prepare_input(self, payload: dict[str, Any]) -> Any:
308
+ """Transform the (post-middleware) payload into the runner's argument.
309
+
310
+ The default passes the payload through unchanged (state-dict frameworks
311
+ such as LangGraph). Prompt-based frameworks override this to return a
312
+ string via :func:`_extract_prompt`.
313
+ """
314
+ return payload
315
+
316
+ def _call_runner(self, runner: Runner, payload: dict[str, Any]) -> Any:
317
+ """Invoke ``runner`` with the prepared input and return the raw result."""
318
+ return runner(self._prepare_input(payload))
319
+
320
+ # -- BaseAdapter contract --------------------------------------------------
321
+
322
+ def validate_agent(self, agent: Any) -> bool:
323
+ """Return ``True`` if a runner can be resolved from ``agent``."""
324
+ return self._resolve_runner(agent) is not None
325
+
326
+ def wrap_agent(self, agent: Any) -> Agent:
327
+ """Wrap a framework agent with ADAPT-Agent governance.
328
+
329
+ Raises:
330
+ AdapterError: If no runnable entry point can be resolved.
331
+ """
332
+ if not self.validate_agent(agent):
333
+ raise AdapterError(
334
+ f"{type(self).__name__}.wrap_agent could not find a runnable entry "
335
+ f"point on the supplied object. Expected one of "
336
+ f"{', '.join(self.run_method_names)!r} or a plain callable. See the "
337
+ f"adapter docstring for the object to wrap."
338
+ )
339
+ return _GovernedAgent(agent, self)
340
+
341
+ def extract_state(self, agent: Any) -> AgentState:
342
+ """Extract a normalized :class:`AgentState` from a state payload or agent.
343
+
344
+ Accepts either a state mapping (e.g. ``{"messages": [...], ...}``) or a
345
+ stateful object exposing ``get_state``. Extraction is best-effort and
346
+ always returns a well-formed :class:`AgentState`.
347
+ """
348
+ raw: Any = agent
349
+ get_state = getattr(agent, "get_state", None)
350
+ if callable(get_state):
351
+ try:
352
+ snapshot = get_state(self.config.get("graph_config", {}))
353
+ raw = getattr(snapshot, "values", snapshot)
354
+ except Exception:
355
+ raw = {}
356
+
357
+ if not isinstance(raw, dict):
358
+ raw = {}
359
+
360
+ messages = raw.get("messages", [])
361
+ if not isinstance(messages, list):
362
+ messages = []
363
+
364
+ context = {k: v for k, v in raw.items() if k != "messages"}
365
+
366
+ state: AgentState = {"messages": messages, "context": context}
367
+ if "trust_score" in raw and isinstance(raw["trust_score"], (int, float)):
368
+ state["trust_score"] = float(raw["trust_score"])
369
+ if "policy_violations" in raw and isinstance(raw["policy_violations"], list):
370
+ state["policy_violations"] = raw["policy_violations"]
371
+ return state
372
+
373
+ def inject_middleware(self, agent: Any, middleware: Any) -> Any:
374
+ """Attach a middleware pipeline and return a freshly wrapped agent."""
375
+ if not isinstance(middleware, Middleware):
376
+ raise AdapterError("inject_middleware expects an adapt_agent.core.Middleware instance.")
377
+ self.middleware = middleware
378
+ return self.wrap_agent(agent)
379
+
380
+ def get_framework_name(self) -> str:
381
+ """Return the human-readable framework name."""
382
+ return self.framework_name
383
+
384
+ # -- internal helpers shared with the wrapped agent ------------------------
385
+
386
+ def _screen_input(self, payload: Any) -> list[str]:
387
+ """Run firewall + adversarial defense over a payload, returning threats."""
388
+ threats: list[str] = []
389
+ for text in _extract_texts(payload):
390
+ if self.firewall is not None and not self.firewall.check_input(text):
391
+ threats.append("firewall")
392
+ if self.defense is not None:
393
+ analysis = self.defense.analyze_input(text)
394
+ if not analysis["is_safe"]:
395
+ threats.extend(analysis["threats_detected"])
396
+ return threats
397
+
398
+ def _screen_output(self, payload: Any) -> list[str]:
399
+ """Run firewall over an output payload, returning threats."""
400
+ threats: list[str] = []
401
+ if self.firewall is None:
402
+ return threats
403
+ for text in _extract_texts(payload):
404
+ if not self.firewall.check_output(text):
405
+ threats.append("firewall")
406
+ return threats
407
+
408
+
409
+ class _GovernedAgent:
410
+ """A framework agent wrapped with ADAPT-Agent governance.
411
+
412
+ Implements the :class:`~adapt_agent.core.types.Agent` protocol (``execute``
413
+ and ``get_state``).
414
+ """
415
+
416
+ def __init__(self, agent: Any, adapter: GovernedAdapter):
417
+ self._agent = agent
418
+ self._adapter = adapter
419
+ # ``wrap_agent`` validates the agent first, so a runner always resolves.
420
+ self._runner: Runner = cast(Runner, adapter._resolve_runner(agent))
421
+ self._last_state: AgentState = {"messages": [], "context": {}}
422
+
423
+ def execute(self, input_data: dict[str, Any]) -> dict[str, Any]:
424
+ """Run the wrapped agent with governance applied.
425
+
426
+ Order of operations: input screening -> policy check -> pre-middleware
427
+ -> traced run -> post-middleware -> output screening.
428
+
429
+ Raises:
430
+ SecurityBlockedError: If a control blocks the input/output (and
431
+ ``block_on_violation`` is enabled).
432
+ """
433
+ adapter = self._adapter
434
+ trace_id = uuid.uuid4().hex
435
+
436
+ # 1. Input screening.
437
+ in_threats = adapter._screen_input(input_data)
438
+ if in_threats and adapter.block_on_violation:
439
+ raise SecurityBlockedError("Input blocked by security controls", in_threats)
440
+
441
+ # 2. Policy enforcement against the extracted state.
442
+ self._last_state = adapter.extract_state(input_data)
443
+ if adapter.policy_enforcer is not None:
444
+ violations = adapter.policy_enforcer.check_state(self._last_state)
445
+ if violations and adapter.block_on_violation:
446
+ blocking = []
447
+ for v in violations:
448
+ rule = adapter.policy_enforcer.get_rule(v)
449
+ if rule is not None and rule.get("action") == "block":
450
+ blocking.append(v)
451
+ if blocking:
452
+ raise SecurityBlockedError(
453
+ "Input blocked by policy", [f"policy:{v}" for v in blocking]
454
+ )
455
+
456
+ # 3. Pre-middleware.
457
+ payload = input_data
458
+ if adapter.middleware is not None:
459
+ payload = adapter.middleware.process_input(input_data)
460
+
461
+ # 4. Traced execution.
462
+ if adapter.observer is not None:
463
+ adapter.observer.start_trace(trace_id, adapter.agent_id, adapter.operation)
464
+ try:
465
+ raw = adapter._call_runner(self._runner, payload)
466
+ result = _resolve_result(raw)
467
+ except Exception as exc:
468
+ if adapter.observer is not None:
469
+ adapter.observer.end_trace(trace_id, status="error", result=str(exc))
470
+ raise
471
+ if adapter.observer is not None:
472
+ adapter.observer.end_trace(trace_id, status="completed")
473
+
474
+ # 5. Post-middleware.
475
+ if adapter.middleware is not None:
476
+ wrapped = adapter.middleware.process_output({"result": result})
477
+ result = wrapped["result"]
478
+
479
+ # 6. Output screening.
480
+ out_threats = adapter._screen_output(result)
481
+ if out_threats and adapter.block_on_violation:
482
+ raise SecurityBlockedError("Output blocked by security controls", out_threats)
483
+
484
+ # Track state from the actual returned payload. Non-dict framework
485
+ # results (AgentRunResult, CrewOutput, ...) are wrapped in {"result": ...}
486
+ # so get_state() reflects the latest execution rather than stale input.
487
+ output_payload = result if isinstance(result, dict) else {"result": result}
488
+ self._last_state = adapter.extract_state(output_payload)
489
+ return output_payload
490
+
491
+ def get_state(self) -> AgentState:
492
+ """Return the most recently observed agent state."""
493
+ return self._last_state
494
+
495
+
496
+ __all__ = ["GovernedAdapter", "_GovernedAgent", "_extract_texts", "_extract_prompt"]