dea-agents 0.1.0__tar.gz

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.
@@ -0,0 +1,15 @@
1
+ node_modules/
2
+ dist/
3
+ coverage/
4
+ *.log
5
+ .DS_Store
6
+ examples/**/node_modules/
7
+ examples/**/__pycache__/
8
+ *.pcap
9
+ *.heapsnapshot
10
+ .vhs-out/
11
+ .lab-dea/
12
+ .lab-dea/
13
+ python/dist/
14
+ python/**/__pycache__/
15
+ python/*.egg-info/
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Shreyas Sawant and DEA contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,107 @@
1
+ Metadata-Version: 2.5
2
+ Name: dea-agents
3
+ Version: 0.1.0
4
+ Summary: Debug Anything from Python agents: observe running processes (strace, py-spy, perf, lsof, tcpdump, gdb/lldb, heap snapshots) and get evidence-backed findings. Wraps the DEA CLI/MCP server; tool adapters for LangChain, LlamaIndex and CrewAI.
5
+ Project-URL: Homepage, https://github.com/shreyassaw5394/dea
6
+ Project-URL: Documentation, https://github.com/shreyassaw5394/dea/tree/main/docs
7
+ Project-URL: Issues, https://github.com/shreyassaw5394/dea/issues
8
+ Author: Shreyas Sawant
9
+ License-Expression: MIT
10
+ License-File: LICENSE
11
+ Keywords: agents,crewai,debugging,langchain,llamaindex,mcp,observability,perf,profiling,py-spy,strace
12
+ Classifier: License :: OSI Approved :: MIT License
13
+ Classifier: Programming Language :: Python :: 3
14
+ Classifier: Topic :: Software Development :: Debuggers
15
+ Classifier: Topic :: System :: Monitoring
16
+ Requires-Python: >=3.9
17
+ Provides-Extra: crewai
18
+ Requires-Dist: crewai>=0.30; extra == 'crewai'
19
+ Requires-Dist: pydantic>=2; extra == 'crewai'
20
+ Provides-Extra: dev
21
+ Requires-Dist: pytest; extra == 'dev'
22
+ Provides-Extra: langchain
23
+ Requires-Dist: langchain-core>=0.2; extra == 'langchain'
24
+ Requires-Dist: pydantic>=2; extra == 'langchain'
25
+ Provides-Extra: llamaindex
26
+ Requires-Dist: llama-index-core>=0.10; extra == 'llamaindex'
27
+ Description-Content-Type: text/markdown
28
+
29
+ # dea-agents (Python)
30
+
31
+ **Debug Anything from Python.** Observe a running process, container or pod —
32
+ syscalls, CPU profiles, thread stacks, sockets, packets, heap retainers — and
33
+ get findings that cite evidence. This package wraps the
34
+ [DEA](https://github.com/shreyassaw5394/dea) CLI (Node ≥ 22 required:
35
+ `npx -y dea-agents` is used automatically) and ships tool adapters for
36
+ LangChain, LlamaIndex and CrewAI.
37
+
38
+ ```bash
39
+ pip install dea-agents # core (no deps)
40
+ pip install 'dea-agents[langchain]' # + LangChain StructuredTools
41
+ pip install 'dea-agents[llamaindex]' # + LlamaIndex FunctionTools
42
+ pip install 'dea-agents[crewai]' # + CrewAI BaseTools
43
+ ```
44
+
45
+ ## Plain Python
46
+
47
+ ```python
48
+ from dea_agents import DEA
49
+
50
+ dea = DEA() # uses `dea` if installed, else `npx -y dea-agents@latest`
51
+ print(dea.readiness()["findings"])
52
+ s = dea.attach(12345) # pid, "my-container", "ns/pod", or a command
53
+ before = dea.snapshot_memory(s)
54
+ # ... exercise the target ...
55
+ after = dea.snapshot_memory(s)
56
+ diff = dea.diff_runs(s, before["run_id"], after["run_id"])
57
+ for line in DEA.findings(diff):
58
+ print(line) # [high] heap_retainer: name=Array grew 2.0 KB → 50.1 MB (observed; e4,e1)
59
+ dea.report(s, out="report.md")
60
+ dea.detach(s)
61
+ ```
62
+
63
+ Every call returns the DEA `ToolResult` dict (`ok`, `findings`, `evidence`,
64
+ `limitations`, `suggested_next`, `artifacts`). `capture_traffic` needs
65
+ `consent=True` — ask your user first.
66
+
67
+ ## LangChain
68
+
69
+ ```python
70
+ from dea_agents.langchain import dea_tools
71
+ from langgraph.prebuilt import create_react_agent
72
+
73
+ agent = create_react_agent(llm, dea_tools())
74
+ agent.invoke({"messages": [("user", "pid 12345 is slow — attach and tell me why, with evidence")]})
75
+ ```
76
+
77
+ ## LlamaIndex
78
+
79
+ ```python
80
+ from dea_agents.llamaindex import dea_tools
81
+ from llama_index.core.agent import ReActAgent
82
+
83
+ agent = ReActAgent.from_tools(dea_tools(), llm=llm, verbose=True)
84
+ ```
85
+
86
+ ## CrewAI
87
+
88
+ ```python
89
+ from dea_agents.crewai import dea_tools
90
+ from crewai import Agent
91
+
92
+ sre = Agent(role="SRE", goal="Diagnose the hung worker with evidence, never guess", tools=dea_tools(), llm=llm)
93
+ ```
94
+
95
+ ## Or just use MCP
96
+
97
+ Any framework with MCP support (LangChain `langchain-mcp-adapters`, LlamaIndex
98
+ `McpToolSpec`, CrewAI `MCPServerAdapter`, Claude Code, Codex, Cursor…) can run
99
+ DEA directly: `{"command": "npx", "args": ["-y", "dea-agents@latest", "serve"]}`.
100
+ Same 14 tools, same ToolResult, plus the guided prompts.
101
+
102
+ ## Safety
103
+
104
+ DEA is read-only, never runs sudo, caps every tool at 60 s, redacts secrets in
105
+ what it returns, and keeps raw artifacts under `~/.dea/sessions/`. See
106
+ [docs/safety.md](https://github.com/shreyassaw5394/dea/blob/main/docs/safety.md).
107
+ MIT.
@@ -0,0 +1,79 @@
1
+ # dea-agents (Python)
2
+
3
+ **Debug Anything from Python.** Observe a running process, container or pod —
4
+ syscalls, CPU profiles, thread stacks, sockets, packets, heap retainers — and
5
+ get findings that cite evidence. This package wraps the
6
+ [DEA](https://github.com/shreyassaw5394/dea) CLI (Node ≥ 22 required:
7
+ `npx -y dea-agents` is used automatically) and ships tool adapters for
8
+ LangChain, LlamaIndex and CrewAI.
9
+
10
+ ```bash
11
+ pip install dea-agents # core (no deps)
12
+ pip install 'dea-agents[langchain]' # + LangChain StructuredTools
13
+ pip install 'dea-agents[llamaindex]' # + LlamaIndex FunctionTools
14
+ pip install 'dea-agents[crewai]' # + CrewAI BaseTools
15
+ ```
16
+
17
+ ## Plain Python
18
+
19
+ ```python
20
+ from dea_agents import DEA
21
+
22
+ dea = DEA() # uses `dea` if installed, else `npx -y dea-agents@latest`
23
+ print(dea.readiness()["findings"])
24
+ s = dea.attach(12345) # pid, "my-container", "ns/pod", or a command
25
+ before = dea.snapshot_memory(s)
26
+ # ... exercise the target ...
27
+ after = dea.snapshot_memory(s)
28
+ diff = dea.diff_runs(s, before["run_id"], after["run_id"])
29
+ for line in DEA.findings(diff):
30
+ print(line) # [high] heap_retainer: name=Array grew 2.0 KB → 50.1 MB (observed; e4,e1)
31
+ dea.report(s, out="report.md")
32
+ dea.detach(s)
33
+ ```
34
+
35
+ Every call returns the DEA `ToolResult` dict (`ok`, `findings`, `evidence`,
36
+ `limitations`, `suggested_next`, `artifacts`). `capture_traffic` needs
37
+ `consent=True` — ask your user first.
38
+
39
+ ## LangChain
40
+
41
+ ```python
42
+ from dea_agents.langchain import dea_tools
43
+ from langgraph.prebuilt import create_react_agent
44
+
45
+ agent = create_react_agent(llm, dea_tools())
46
+ agent.invoke({"messages": [("user", "pid 12345 is slow — attach and tell me why, with evidence")]})
47
+ ```
48
+
49
+ ## LlamaIndex
50
+
51
+ ```python
52
+ from dea_agents.llamaindex import dea_tools
53
+ from llama_index.core.agent import ReActAgent
54
+
55
+ agent = ReActAgent.from_tools(dea_tools(), llm=llm, verbose=True)
56
+ ```
57
+
58
+ ## CrewAI
59
+
60
+ ```python
61
+ from dea_agents.crewai import dea_tools
62
+ from crewai import Agent
63
+
64
+ sre = Agent(role="SRE", goal="Diagnose the hung worker with evidence, never guess", tools=dea_tools(), llm=llm)
65
+ ```
66
+
67
+ ## Or just use MCP
68
+
69
+ Any framework with MCP support (LangChain `langchain-mcp-adapters`, LlamaIndex
70
+ `McpToolSpec`, CrewAI `MCPServerAdapter`, Claude Code, Codex, Cursor…) can run
71
+ DEA directly: `{"command": "npx", "args": ["-y", "dea-agents@latest", "serve"]}`.
72
+ Same 14 tools, same ToolResult, plus the guided prompts.
73
+
74
+ ## Safety
75
+
76
+ DEA is read-only, never runs sudo, caps every tool at 60 s, redacts secrets in
77
+ what it returns, and keeps raw artifacts under `~/.dea/sessions/`. See
78
+ [docs/safety.md](https://github.com/shreyassaw5394/dea/blob/main/docs/safety.md).
79
+ MIT.
@@ -0,0 +1,152 @@
1
+ """dea-agents — Debug Anything from Python.
2
+
3
+ Thin, dependency-free wrapper around the DEA CLI (``npx -y dea-agents``). Every
4
+ method returns the DEA ``ToolResult`` dict: ``findings`` cite ``evidence`` ids,
5
+ ``limitations`` say what could not be observed, ``artifacts`` point at raw
6
+ files under ``~/.dea/sessions/<id>/``.
7
+
8
+ from dea_agents import DEA
9
+ dea = DEA()
10
+ s = dea.attach(12345)
11
+ print(dea.profile_cpu(s, duration_s=10)["findings"])
12
+ dea.detach(s)
13
+
14
+ Framework adapters: ``dea_agents.langchain``, ``dea_agents.llamaindex``,
15
+ ``dea_agents.crewai``. Agents that speak MCP can skip this package and run
16
+ ``npx -y dea-agents serve`` directly.
17
+ """
18
+ from __future__ import annotations
19
+
20
+ import json
21
+ import os
22
+ import shutil
23
+ import subprocess
24
+ from typing import Any, Dict, List, Optional, Sequence
25
+
26
+ __version__ = "0.1.0"
27
+ __all__ = ["DEA", "DEAError", "TOOLS", "ToolResult"]
28
+
29
+ ToolResult = Dict[str, Any]
30
+
31
+ TOOLS: Dict[str, str] = {
32
+ "readiness": "OS, arch, which providers are usable, what is missing and how to install it, permission status. Call first.",
33
+ "attach": "Resolve a target (pid, container, pod, or a command to spawn) and open a session. Returns session_id.",
34
+ "detach": "Stop captures, write the report, keep artifacts.",
35
+ "list_processes": "ps-style list (pid, comm, cpu, mem, user, container) with optional substring filter.",
36
+ "trace_syscalls": "Linux strace for N seconds: top syscalls by time/count, slowest calls, error counts, blocked waits.",
37
+ "profile_cpu": "CPU profile (py-spy / perf / sample): top frames, hottest stacks, flamegraph.svg artifact.",
38
+ "dump_stacks": "Every thread's stack; flags lock waits and likely deadlocks.",
39
+ "inspect_sockets": "Listening ports, connections by remote, CLOSE_WAIT/TIME_WAIT pileups, fd count vs ulimit.",
40
+ "inspect_files": "Open fds by type, fd growth over a window, largest and deleted-but-open files.",
41
+ "capture_traffic": "tcpdump scoped to the target's ports (needs consent=True): per-remote bytes, RSTs, retransmits, DNS.",
42
+ "snapshot_memory": "RSS series; Node heap snapshot with top retainers; Python stacks with locals.",
43
+ "diff_runs": "Diff two prior runs (e.g. snapshot_memory#1 vs #2): what grew, appeared, disappeared.",
44
+ "container_info": "docker inspect/stats/top/logs, cgroup limits, OOM kills, restarts.",
45
+ "report": "Markdown report of the whole session.",
46
+ }
47
+
48
+
49
+ class DEAError(RuntimeError):
50
+ """Raised when the DEA CLI cannot be executed at all (Node/npx missing, bad JSON)."""
51
+
52
+
53
+ class DEA:
54
+ """Client for the DEA CLI.
55
+
56
+ Args:
57
+ command: how to invoke DEA. Defaults to ``["npx", "-y", "dea-agents@latest"]``;
58
+ pass ``["dea"]`` if installed globally. ``DEA_COMMAND`` env var (shell-split) overrides.
59
+ timeout: subprocess timeout in seconds (DEA itself caps tools at 60 s).
60
+ raise_on_error: raise ``DEAError`` when ``ok`` is false instead of returning the result.
61
+ """
62
+
63
+ def __init__(self, command: Optional[Sequence[str]] = None, timeout: float = 120.0, raise_on_error: bool = False) -> None:
64
+ env_cmd = os.environ.get("DEA_COMMAND")
65
+ if command is None and env_cmd:
66
+ command = env_cmd.split()
67
+ if command is None:
68
+ command = ["dea"] if shutil.which("dea") else ["npx", "-y", "dea-agents@latest"]
69
+ self.command: List[str] = list(command)
70
+ self.timeout = timeout
71
+ self.raise_on_error = raise_on_error
72
+
73
+ # -- core ---------------------------------------------------------------
74
+
75
+ def call(self, tool: str, **args: Any) -> ToolResult:
76
+ """Call any DEA tool by name. Returns the ToolResult dict."""
77
+ if tool not in TOOLS:
78
+ raise DEAError(f"unknown tool {tool!r}; known: {', '.join(TOOLS)}")
79
+ argv = [*self.command, "call", tool, json.dumps({k: v for k, v in args.items() if v is not None})]
80
+ try:
81
+ proc = subprocess.run(argv, capture_output=True, text=True, timeout=self.timeout, check=False)
82
+ except FileNotFoundError as e:
83
+ raise DEAError(f"cannot run {argv[0]!r}: install Node.js >= 22 (https://nodejs.org) or set DEA_COMMAND") from e
84
+ except subprocess.TimeoutExpired as e:
85
+ raise DEAError(f"{tool} timed out after {self.timeout}s") from e
86
+ out = proc.stdout.strip()
87
+ start = out.find("{")
88
+ if start < 0:
89
+ raise DEAError(f"DEA produced no JSON for {tool}: {proc.stderr.strip()[:500]}")
90
+ try:
91
+ result: ToolResult = json.loads(out[start:])
92
+ except json.JSONDecodeError as e:
93
+ raise DEAError(f"bad JSON from DEA for {tool}: {e}") from e
94
+ if self.raise_on_error and not result.get("ok", False):
95
+ raise DEAError(result.get("error") or f"{tool} failed")
96
+ return result
97
+
98
+ # -- convenience --------------------------------------------------------
99
+
100
+ def readiness(self) -> ToolResult:
101
+ return self.call("readiness")
102
+
103
+ def list_processes(self, filter: Optional[str] = None, limit: int = 100) -> ToolResult:
104
+ return self.call("list_processes", filter=filter, limit=limit)
105
+
106
+ def attach(self, target: Any, kind: Optional[str] = None) -> str:
107
+ """Attach and return the session_id. Raises DEAError if attach failed."""
108
+ r = self.call("attach", target=str(target), kind=kind)
109
+ if not r.get("ok"):
110
+ raise DEAError(r.get("error") or "attach failed")
111
+ return str(r["session_id"])
112
+
113
+ def detach(self, session_id: str) -> ToolResult:
114
+ return self.call("detach", session_id=session_id)
115
+
116
+ def trace_syscalls(self, session_id: str, duration_s: int = 10, categories: Optional[Sequence[str]] = None) -> ToolResult:
117
+ return self.call("trace_syscalls", session_id=session_id, duration_s=duration_s, categories=list(categories) if categories else None)
118
+
119
+ def profile_cpu(self, session_id: str, duration_s: int = 15) -> ToolResult:
120
+ return self.call("profile_cpu", session_id=session_id, duration_s=duration_s)
121
+
122
+ def dump_stacks(self, session_id: str) -> ToolResult:
123
+ return self.call("dump_stacks", session_id=session_id)
124
+
125
+ def inspect_sockets(self, session_id: str) -> ToolResult:
126
+ return self.call("inspect_sockets", session_id=session_id)
127
+
128
+ def inspect_files(self, session_id: str, window_s: int = 3) -> ToolResult:
129
+ return self.call("inspect_files", session_id=session_id, window_s=window_s)
130
+
131
+ def capture_traffic(self, session_id: str, duration_s: int = 10, filter: Optional[str] = None, consent: bool = False) -> ToolResult:
132
+ """Requires consent=True — ask the user first; captures can contain secrets."""
133
+ return self.call("capture_traffic", session_id=session_id, duration_s=duration_s, filter=filter, consent=consent)
134
+
135
+ def snapshot_memory(self, session_id: str, heap: bool = True, window_s: int = 3) -> ToolResult:
136
+ return self.call("snapshot_memory", session_id=session_id, heap=heap, window_s=window_s)
137
+
138
+ def diff_runs(self, session_id: str, a: str, b: str) -> ToolResult:
139
+ return self.call("diff_runs", session_id=session_id, a=a, b=b)
140
+
141
+ def container_info(self, session_id: str) -> ToolResult:
142
+ return self.call("container_info", session_id=session_id)
143
+
144
+ def report(self, session_id: str, out: Optional[str] = None) -> ToolResult:
145
+ return self.call("report", session_id=session_id, out=out)
146
+
147
+ # -- helpers ------------------------------------------------------------
148
+
149
+ @staticmethod
150
+ def findings(result: ToolResult) -> List[str]:
151
+ """Human-readable finding lines with their evidence ids."""
152
+ return [f"[{f['severity']}] {f['summary']} ({f['confidence']}; {','.join(f['evidence_ids'])})" for f in result.get("findings", [])]
@@ -0,0 +1,41 @@
1
+ """Shared argument schemas for framework adapters (mirrors src/contracts.ts ToolArgs)."""
2
+ from __future__ import annotations
3
+
4
+ from typing import Any, Dict
5
+
6
+ SESSION = {"session_id": {"type": "string", "description": "session id from attach"}}
7
+
8
+ SPECS: Dict[str, Dict[str, Any]] = {
9
+ "readiness": {},
10
+ "attach": {
11
+ "target": {"type": "string", "description": "pid, container name, k8s pod (ns/name) or a command line"},
12
+ "kind": {"type": "string", "enum": ["pid", "container", "pod", "command"], "description": "default: auto-detect"},
13
+ },
14
+ "detach": {**SESSION},
15
+ "list_processes": {"filter": {"type": "string"}, "limit": {"type": "integer", "default": 100}},
16
+ "trace_syscalls": {**SESSION, "duration_s": {"type": "integer", "default": 10, "maximum": 60}, "categories": {"type": "array", "items": {"type": "string", "enum": ["network", "file", "process", "signal", "memory", "all"]}}},
17
+ "profile_cpu": {**SESSION, "duration_s": {"type": "integer", "default": 15, "maximum": 60}},
18
+ "dump_stacks": {**SESSION},
19
+ "inspect_sockets": {**SESSION},
20
+ "inspect_files": {**SESSION, "window_s": {"type": "integer", "default": 3}},
21
+ "capture_traffic": {**SESSION, "duration_s": {"type": "integer", "default": 10, "maximum": 30}, "filter": {"type": "string"}, "consent": {"type": "boolean", "description": "REQUIRED true; ask the user first"}},
22
+ "snapshot_memory": {**SESSION, "heap": {"type": "boolean", "default": True}, "window_s": {"type": "integer", "default": 3}},
23
+ "diff_runs": {**SESSION, "a": {"type": "string", "description": "earlier run id, e.g. snapshot_memory#2"}, "b": {"type": "string", "description": "later run id"}},
24
+ "container_info": {**SESSION},
25
+ "report": {**SESSION, "out": {"type": "string", "description": "optional path to also write the markdown to"}},
26
+ }
27
+
28
+ REQUIRED: Dict[str, list] = {
29
+ "attach": ["target"],
30
+ "diff_runs": ["session_id", "a", "b"],
31
+ "capture_traffic": ["session_id", "consent"],
32
+ }
33
+ for _name, _spec in SPECS.items():
34
+ if "session_id" in _spec and _name not in REQUIRED:
35
+ REQUIRED[_name] = ["session_id"]
36
+ REQUIRED.setdefault("readiness", [])
37
+ REQUIRED.setdefault("list_processes", [])
38
+
39
+
40
+ def json_schema(tool: str) -> Dict[str, Any]:
41
+ return {"type": "object", "properties": SPECS[tool], "required": REQUIRED.get(tool, [])}
@@ -0,0 +1,51 @@
1
+ """CrewAI tools for DEA.
2
+
3
+ from dea_agents.crewai import dea_tools
4
+ agent = Agent(role="SRE", goal="diagnose the slow API with evidence", tools=dea_tools(), ...)
5
+
6
+ Requires ``pip install dea-agents[crewai]``.
7
+ """
8
+ from __future__ import annotations
9
+
10
+ import json
11
+ from typing import Any, List, Optional, Type
12
+
13
+ from . import DEA, TOOLS
14
+ from ._specs import SPECS, REQUIRED
15
+
16
+ try:
17
+ from crewai.tools import BaseTool
18
+ from pydantic import BaseModel, Field, create_model
19
+ except ImportError as e: # pragma: no cover
20
+ raise ImportError("pip install 'dea-agents[crewai]'") from e
21
+
22
+ _PY = {"string": str, "integer": int, "boolean": bool, "array": List[str]}
23
+
24
+
25
+ def _model(tool: str) -> Type[BaseModel]:
26
+ fields: dict = {}
27
+ for name, spec in SPECS[tool].items():
28
+ typ = _PY.get(spec.get("type", "string"), str)
29
+ required = name in REQUIRED.get(tool, [])
30
+ fields[name] = (typ if required else Optional[typ], Field(... if required else spec.get("default", None), description=spec.get("description", "")))
31
+ return create_model(f"Dea{tool.title().replace('_', '')}Args", **fields) # type: ignore[call-overload]
32
+
33
+
34
+ def dea_tools(client: Optional[DEA] = None, include: Optional[List[str]] = None) -> List["BaseTool"]:
35
+ dea = client or DEA()
36
+ out: List[BaseTool] = []
37
+ for tool_name, desc in TOOLS.items():
38
+ if include and tool_name not in include:
39
+ continue
40
+
41
+ class _Tool(BaseTool):
42
+ name: str = f"dea_{tool_name}"
43
+ description: str = desc + " Findings cite evidence ids; never state a cause without one."
44
+ args_schema: Type[BaseModel] = _model(tool_name)
45
+ _dea_tool: str = tool_name
46
+
47
+ def _run(self, **kwargs: Any) -> str:
48
+ return json.dumps(dea.call(self._dea_tool, **kwargs))
49
+
50
+ out.append(_Tool())
51
+ return out
@@ -0,0 +1,48 @@
1
+ """LangChain tools for DEA.
2
+
3
+ from dea_agents.langchain import dea_tools
4
+ tools = dea_tools() # 14 StructuredTools, one per DEA tool
5
+ agent = create_react_agent(llm, tools)
6
+
7
+ Each tool returns the ToolResult as a JSON string. Requires ``pip install dea-agents[langchain]``.
8
+ """
9
+ from __future__ import annotations
10
+
11
+ import json
12
+ from typing import Any, List, Optional, Type
13
+
14
+ from . import DEA, TOOLS
15
+ from ._specs import SPECS, REQUIRED
16
+
17
+ try:
18
+ from langchain_core.tools import StructuredTool
19
+ from pydantic import BaseModel, Field, create_model
20
+ except ImportError as e: # pragma: no cover
21
+ raise ImportError("pip install 'dea-agents[langchain]'") from e
22
+
23
+ _PY = {"string": str, "integer": int, "boolean": bool, "array": List[str]}
24
+
25
+
26
+ def _model(tool: str) -> Type[BaseModel]:
27
+ fields: dict = {}
28
+ for name, spec in SPECS[tool].items():
29
+ typ = _PY.get(spec.get("type", "string"), str)
30
+ required = name in REQUIRED.get(tool, [])
31
+ default = ... if required else spec.get("default", None)
32
+ fields[name] = (typ if required else Optional[typ], Field(default, description=spec.get("description", "")))
33
+ return create_model(f"Dea{tool.title().replace('_', '')}Args", **fields) # type: ignore[call-overload]
34
+
35
+
36
+ def dea_tools(client: Optional[DEA] = None, include: Optional[List[str]] = None) -> List["StructuredTool"]:
37
+ """Build one LangChain StructuredTool per DEA tool."""
38
+ dea = client or DEA()
39
+ out: List[StructuredTool] = []
40
+ for name, desc in TOOLS.items():
41
+ if include and name not in include:
42
+ continue
43
+
44
+ def _run(_name: str = name, **kwargs: Any) -> str:
45
+ return json.dumps(dea.call(_name, **kwargs))
46
+
47
+ out.append(StructuredTool.from_function(func=_run, name=f"dea_{name}", description=desc + " Findings cite evidence ids; never state a cause without one.", args_schema=_model(name)))
48
+ return out
@@ -0,0 +1,33 @@
1
+ """LlamaIndex FunctionTools for DEA.
2
+
3
+ from dea_agents.llamaindex import dea_tools
4
+ agent = ReActAgent.from_tools(dea_tools(), llm=llm)
5
+
6
+ Requires ``pip install dea-agents[llamaindex]``.
7
+ """
8
+ from __future__ import annotations
9
+
10
+ import json
11
+ from typing import Any, List, Optional
12
+
13
+ from . import DEA, TOOLS
14
+
15
+ try:
16
+ from llama_index.core.tools import FunctionTool
17
+ except ImportError as e: # pragma: no cover
18
+ raise ImportError("pip install 'dea-agents[llamaindex]'") from e
19
+
20
+
21
+ def dea_tools(client: Optional[DEA] = None, include: Optional[List[str]] = None) -> List["FunctionTool"]:
22
+ dea = client or DEA()
23
+ out: List[FunctionTool] = []
24
+ for name, desc in TOOLS.items():
25
+ if include and name not in include:
26
+ continue
27
+
28
+ def _run(_name: str = name, **kwargs: Any) -> str:
29
+ """Run a DEA tool. Pass arguments as keyword arguments (session_id, duration_s, ...)."""
30
+ return json.dumps(dea.call(_name, **kwargs))
31
+
32
+ out.append(FunctionTool.from_defaults(fn=_run, name=f"dea_{name}", description=desc + " Findings cite evidence ids."))
33
+ return out
@@ -0,0 +1,34 @@
1
+ [build-system]
2
+ requires = ["hatchling"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "dea-agents"
7
+ version = "0.1.0"
8
+ description = "Debug Anything from Python agents: observe running processes (strace, py-spy, perf, lsof, tcpdump, gdb/lldb, heap snapshots) and get evidence-backed findings. Wraps the DEA CLI/MCP server; tool adapters for LangChain, LlamaIndex and CrewAI."
9
+ readme = "README.md"
10
+ license = "MIT"
11
+ requires-python = ">=3.9"
12
+ authors = [{ name = "Shreyas Sawant" }]
13
+ keywords = ["debugging", "observability", "profiling", "agents", "mcp", "langchain", "llamaindex", "crewai", "strace", "py-spy", "perf"]
14
+ classifiers = [
15
+ "Programming Language :: Python :: 3",
16
+ "License :: OSI Approved :: MIT License",
17
+ "Topic :: Software Development :: Debuggers",
18
+ "Topic :: System :: Monitoring",
19
+ ]
20
+ dependencies = []
21
+
22
+ [project.optional-dependencies]
23
+ langchain = ["langchain-core>=0.2", "pydantic>=2"]
24
+ llamaindex = ["llama-index-core>=0.10"]
25
+ crewai = ["crewai>=0.30", "pydantic>=2"]
26
+ dev = ["pytest"]
27
+
28
+ [project.urls]
29
+ Homepage = "https://github.com/shreyassaw5394/dea"
30
+ Documentation = "https://github.com/shreyassaw5394/dea/tree/main/docs"
31
+ Issues = "https://github.com/shreyassaw5394/dea/issues"
32
+
33
+ [tool.hatch.build.targets.wheel]
34
+ packages = ["dea_agents"]
@@ -0,0 +1,49 @@
1
+ import json
2
+ import subprocess
3
+ import sys
4
+ from unittest import mock
5
+
6
+ import pytest
7
+
8
+ from dea_agents import DEA, DEAError, TOOLS
9
+ from dea_agents._specs import SPECS, REQUIRED, json_schema
10
+
11
+
12
+ def test_specs_cover_every_tool():
13
+ assert set(SPECS) == set(TOOLS)
14
+ for t in TOOLS:
15
+ assert json_schema(t)["type"] == "object"
16
+ assert REQUIRED["capture_traffic"] == ["session_id", "consent"]
17
+
18
+
19
+ def test_call_builds_json_args_and_parses_result():
20
+ fake = subprocess.CompletedProcess(args=[], returncode=0, stdout=json.dumps({"ok": True, "tool": "readiness", "findings": [], "evidence": []}), stderr="")
21
+ with mock.patch("subprocess.run", return_value=fake) as run:
22
+ r = DEA(command=["dea"]).call("trace_syscalls", session_id="s", duration_s=5, categories=None)
23
+ assert r["ok"] is True
24
+ argv = run.call_args[0][0]
25
+ assert argv[:3] == ["dea", "call", "trace_syscalls"]
26
+ assert json.loads(argv[3]) == {"session_id": "s", "duration_s": 5}
27
+
28
+
29
+ def test_unknown_tool_rejected():
30
+ with pytest.raises(DEAError):
31
+ DEA(command=["dea"]).call("rm_rf")
32
+
33
+
34
+ def test_attach_returns_session_id():
35
+ fake = subprocess.CompletedProcess(args=[], returncode=0, stdout=json.dumps({"ok": True, "session_id": "abc"}), stderr="")
36
+ with mock.patch("subprocess.run", return_value=fake):
37
+ assert DEA(command=["dea"]).attach(123) == "abc"
38
+
39
+
40
+ def test_missing_node_gives_helpful_error():
41
+ with mock.patch("subprocess.run", side_effect=FileNotFoundError()):
42
+ with pytest.raises(DEAError, match="Node.js"):
43
+ DEA(command=["definitely-not-here"]).readiness()
44
+
45
+
46
+ @pytest.mark.skipif(sys.platform == "win32", reason="not supported")
47
+ def test_findings_formatter():
48
+ r = {"findings": [{"severity": "high", "summary": "x", "confidence": "observed", "evidence_ids": ["e1"]}]}
49
+ assert DEA.findings(r) == ["[high] x (observed; e1)"]