agentguard47 0.2.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,135 @@
1
+ Metadata-Version: 2.4
2
+ Name: agentguard47
3
+ Version: 0.2.0
4
+ Summary: Lightweight observability and evaluation primitives for multi-agent systems
5
+ Author: AgentGuard
6
+ License-Expression: MIT
7
+ Project-URL: Homepage, https://github.com/bmdhodl/agent47
8
+ Project-URL: Repository, https://github.com/bmdhodl/agent47
9
+ Project-URL: Issues, https://github.com/bmdhodl/agent47/issues
10
+ Keywords: agents,observability,tracing,multi-agent,llm,guardrails,replay
11
+ Classifier: Development Status :: 3 - Alpha
12
+ Classifier: Intended Audience :: Developers
13
+ Classifier: Programming Language :: Python :: 3
14
+ Classifier: Programming Language :: Python :: 3.9
15
+ Classifier: Programming Language :: Python :: 3.10
16
+ Classifier: Programming Language :: Python :: 3.11
17
+ Classifier: Programming Language :: Python :: 3.12
18
+ Classifier: Topic :: Software Development :: Libraries
19
+ Classifier: Topic :: Software Development :: Testing
20
+ Classifier: Topic :: System :: Monitoring
21
+ Requires-Python: >=3.9
22
+ Description-Content-Type: text/markdown
23
+ Provides-Extra: langchain
24
+ Requires-Dist: langchain-core>=0.1; extra == "langchain"
25
+
26
+ # AgentGuard SDK (Python)
27
+
28
+ [![PyPI](https://img.shields.io/pypi/v/agentguard47)](https://pypi.org/project/agentguard47/)
29
+ [![License: MIT](https://img.shields.io/badge/License-MIT-green.svg)](https://github.com/bmdhodl/agent47/blob/main/LICENSE)
30
+
31
+ Lightweight, zero-dependency observability for multi-agent AI systems. Trace reasoning steps, catch loops, guard budgets, and replay runs deterministically.
32
+
33
+ ## Install
34
+
35
+ ```bash
36
+ pip install agentguard47
37
+ ```
38
+
39
+ With LangChain support:
40
+ ```bash
41
+ pip install agentguard47[langchain]
42
+ ```
43
+
44
+ ## Quickstart
45
+
46
+ ```python
47
+ from agentguard import Tracer, LoopGuard, BudgetGuard
48
+
49
+ tracer = Tracer()
50
+ loop_guard = LoopGuard(max_repeats=3)
51
+ budget_guard = BudgetGuard(max_tokens=10000)
52
+
53
+ with tracer.trace("agent.run") as span:
54
+ span.event("reasoning.step", data={"thought": "search docs"})
55
+
56
+ loop_guard.check(tool_name="search", tool_args={"query": "agent loops"})
57
+ budget_guard.record_tokens(150)
58
+
59
+ with span.span("tool.call", data={"tool": "search"}):
60
+ pass # call your tool here
61
+ ```
62
+
63
+ ## Tracing
64
+
65
+ ```python
66
+ from agentguard.tracing import Tracer
67
+
68
+ tracer = Tracer()
69
+
70
+ with tracer.trace("agent.run", data={"user_id": "u123"}) as span:
71
+ span.event("reasoning.step", data={"step": 1, "thought": "search docs"})
72
+ with span.span("tool.call", data={"tool": "search", "query": "agent loops"}):
73
+ pass
74
+ ```
75
+
76
+ ## Guards
77
+
78
+ ```python
79
+ from agentguard.guards import LoopGuard, BudgetGuard, TimeoutGuard
80
+
81
+ # Detect repeated tool calls
82
+ guard = LoopGuard(max_repeats=3)
83
+ guard.check(tool_name="search", tool_args={"query": "agent loops"})
84
+
85
+ # Track token and call budgets
86
+ budget = BudgetGuard(max_tokens=50000, max_calls=100)
87
+ budget.record_tokens(150)
88
+ budget.record_call()
89
+
90
+ # Enforce wall-clock time limits
91
+ timeout = TimeoutGuard(max_seconds=30)
92
+ timeout.start()
93
+ timeout.check() # raises TimeoutExceeded if over limit
94
+ ```
95
+
96
+ ## Replay
97
+
98
+ ```python
99
+ from agentguard.recording import Recorder, Replayer
100
+
101
+ recorder = Recorder("runs.jsonl")
102
+ recorder.record_call("llm", {"prompt": "hi"}, {"text": "hello"})
103
+
104
+ replayer = Replayer("runs.jsonl")
105
+ resp = replayer.replay_call("llm", {"prompt": "hi"})
106
+ ```
107
+
108
+ ## CLI
109
+
110
+ ```bash
111
+ # Summarize trace events
112
+ agentguard summarize traces.jsonl
113
+
114
+ # Human-readable report
115
+ agentguard report traces.jsonl
116
+
117
+ # Open trace viewer in browser
118
+ agentguard view traces.jsonl
119
+ ```
120
+
121
+ ## Trace Viewer
122
+
123
+ ```bash
124
+ agentguard view traces.jsonl --port 8080
125
+ ```
126
+
127
+ ## Integrations
128
+
129
+ - LangChain: `agentguard.integrations.langchain`
130
+
131
+ ## Links
132
+
133
+ - [GitHub](https://github.com/bmdhodl/agent47)
134
+ - [Trace Schema](https://github.com/bmdhodl/agent47/blob/main/docs/trace_schema.md)
135
+ - [Examples](https://github.com/bmdhodl/agent47/tree/main/sdk/examples)
@@ -0,0 +1,110 @@
1
+ # AgentGuard SDK (Python)
2
+
3
+ [![PyPI](https://img.shields.io/pypi/v/agentguard47)](https://pypi.org/project/agentguard47/)
4
+ [![License: MIT](https://img.shields.io/badge/License-MIT-green.svg)](https://github.com/bmdhodl/agent47/blob/main/LICENSE)
5
+
6
+ Lightweight, zero-dependency observability for multi-agent AI systems. Trace reasoning steps, catch loops, guard budgets, and replay runs deterministically.
7
+
8
+ ## Install
9
+
10
+ ```bash
11
+ pip install agentguard47
12
+ ```
13
+
14
+ With LangChain support:
15
+ ```bash
16
+ pip install agentguard47[langchain]
17
+ ```
18
+
19
+ ## Quickstart
20
+
21
+ ```python
22
+ from agentguard import Tracer, LoopGuard, BudgetGuard
23
+
24
+ tracer = Tracer()
25
+ loop_guard = LoopGuard(max_repeats=3)
26
+ budget_guard = BudgetGuard(max_tokens=10000)
27
+
28
+ with tracer.trace("agent.run") as span:
29
+ span.event("reasoning.step", data={"thought": "search docs"})
30
+
31
+ loop_guard.check(tool_name="search", tool_args={"query": "agent loops"})
32
+ budget_guard.record_tokens(150)
33
+
34
+ with span.span("tool.call", data={"tool": "search"}):
35
+ pass # call your tool here
36
+ ```
37
+
38
+ ## Tracing
39
+
40
+ ```python
41
+ from agentguard.tracing import Tracer
42
+
43
+ tracer = Tracer()
44
+
45
+ with tracer.trace("agent.run", data={"user_id": "u123"}) as span:
46
+ span.event("reasoning.step", data={"step": 1, "thought": "search docs"})
47
+ with span.span("tool.call", data={"tool": "search", "query": "agent loops"}):
48
+ pass
49
+ ```
50
+
51
+ ## Guards
52
+
53
+ ```python
54
+ from agentguard.guards import LoopGuard, BudgetGuard, TimeoutGuard
55
+
56
+ # Detect repeated tool calls
57
+ guard = LoopGuard(max_repeats=3)
58
+ guard.check(tool_name="search", tool_args={"query": "agent loops"})
59
+
60
+ # Track token and call budgets
61
+ budget = BudgetGuard(max_tokens=50000, max_calls=100)
62
+ budget.record_tokens(150)
63
+ budget.record_call()
64
+
65
+ # Enforce wall-clock time limits
66
+ timeout = TimeoutGuard(max_seconds=30)
67
+ timeout.start()
68
+ timeout.check() # raises TimeoutExceeded if over limit
69
+ ```
70
+
71
+ ## Replay
72
+
73
+ ```python
74
+ from agentguard.recording import Recorder, Replayer
75
+
76
+ recorder = Recorder("runs.jsonl")
77
+ recorder.record_call("llm", {"prompt": "hi"}, {"text": "hello"})
78
+
79
+ replayer = Replayer("runs.jsonl")
80
+ resp = replayer.replay_call("llm", {"prompt": "hi"})
81
+ ```
82
+
83
+ ## CLI
84
+
85
+ ```bash
86
+ # Summarize trace events
87
+ agentguard summarize traces.jsonl
88
+
89
+ # Human-readable report
90
+ agentguard report traces.jsonl
91
+
92
+ # Open trace viewer in browser
93
+ agentguard view traces.jsonl
94
+ ```
95
+
96
+ ## Trace Viewer
97
+
98
+ ```bash
99
+ agentguard view traces.jsonl --port 8080
100
+ ```
101
+
102
+ ## Integrations
103
+
104
+ - LangChain: `agentguard.integrations.langchain`
105
+
106
+ ## Links
107
+
108
+ - [GitHub](https://github.com/bmdhodl/agent47)
109
+ - [Trace Schema](https://github.com/bmdhodl/agent47/blob/main/docs/trace_schema.md)
110
+ - [Examples](https://github.com/bmdhodl/agent47/tree/main/sdk/examples)
@@ -0,0 +1,24 @@
1
+ from .tracing import Tracer
2
+ from .guards import (
3
+ LoopGuard,
4
+ BudgetGuard,
5
+ TimeoutGuard,
6
+ LoopDetected,
7
+ BudgetExceeded,
8
+ TimeoutExceeded,
9
+ )
10
+ from .recording import Recorder, Replayer
11
+ from .sinks import HttpSink
12
+
13
+ __all__ = [
14
+ "Tracer",
15
+ "LoopGuard",
16
+ "BudgetGuard",
17
+ "TimeoutGuard",
18
+ "LoopDetected",
19
+ "BudgetExceeded",
20
+ "TimeoutExceeded",
21
+ "Recorder",
22
+ "Replayer",
23
+ "HttpSink",
24
+ ]
@@ -0,0 +1,113 @@
1
+ from __future__ import annotations
2
+
3
+ import argparse
4
+ import json
5
+ from collections import Counter
6
+ from typing import Any, Dict, List, Optional
7
+
8
+
9
+ def _summarize(path: str) -> None:
10
+ total = 0
11
+ name_counts = Counter()
12
+ kind_counts = Counter()
13
+
14
+ with open(path, "r", encoding="utf-8") as f:
15
+ for line in f:
16
+ line = line.strip()
17
+ if not line:
18
+ continue
19
+ try:
20
+ event: Dict[str, Any] = json.loads(line)
21
+ except json.JSONDecodeError:
22
+ continue
23
+ total += 1
24
+ name = event.get("name", "(unknown)")
25
+ kind = event.get("kind", "(unknown)")
26
+ name_counts[name] += 1
27
+ kind_counts[kind] += 1
28
+
29
+ print(f"events: {total}")
30
+ print("kinds:")
31
+ for kind, count in kind_counts.most_common():
32
+ print(f" {kind}: {count}")
33
+ print("names:")
34
+ for name, count in name_counts.most_common(10):
35
+ print(f" {name}: {count}")
36
+
37
+
38
+ def _report(path: str) -> None:
39
+ events: List[Dict[str, Any]] = []
40
+ with open(path, "r", encoding="utf-8") as f:
41
+ for line in f:
42
+ line = line.strip()
43
+ if not line:
44
+ continue
45
+ try:
46
+ events.append(json.loads(line))
47
+ except json.JSONDecodeError:
48
+ continue
49
+
50
+ if not events:
51
+ print("No events found.")
52
+ return
53
+
54
+ total = len(events)
55
+ kinds = Counter(e.get("kind", "(unknown)") for e in events)
56
+ names = Counter(e.get("name", "(unknown)") for e in events)
57
+ loop_hits = names.get("guard.loop_detected", 0)
58
+
59
+ span_durations: List[float] = []
60
+ for e in events:
61
+ if e.get("kind") == "span" and e.get("phase") == "end":
62
+ dur = e.get("duration_ms")
63
+ if isinstance(dur, (int, float)):
64
+ span_durations.append(float(dur))
65
+
66
+ total_ms: Optional[float] = None
67
+ if span_durations:
68
+ total_ms = max(span_durations)
69
+
70
+ print("AgentGuard report")
71
+ print(f" Total events: {total}")
72
+ print(f" Spans: {kinds.get('span', 0)} Events: {kinds.get('event', 0)}")
73
+ if total_ms is not None:
74
+ print(f" Approx run time: {total_ms:.1f} ms")
75
+ print(f" Reasoning steps: {names.get('reasoning.step', 0)}")
76
+ print(f" Tool results: {names.get('tool.result', 0)}")
77
+ print(f" LLM results: {names.get('llm.result', 0)}")
78
+ if loop_hits:
79
+ print(f" Loop guard triggered: {loop_hits} time(s)")
80
+ else:
81
+ print(" Loop guard triggered: 0")
82
+
83
+
84
+ def main() -> None:
85
+ parser = argparse.ArgumentParser(prog="agentguard")
86
+ sub = parser.add_subparsers(dest="cmd")
87
+
88
+ summarize = sub.add_parser("summarize", help="Summarize a JSONL trace file")
89
+ summarize.add_argument("path")
90
+
91
+ report = sub.add_parser("report", help="Human-readable report for a JSONL trace file")
92
+ report.add_argument("path")
93
+
94
+ view = sub.add_parser("view", help="Open a local trace viewer in the browser")
95
+ view.add_argument("path")
96
+ view.add_argument("--port", type=int, default=8080)
97
+ view.add_argument("--no-open", action="store_true")
98
+
99
+ args = parser.parse_args()
100
+ if args.cmd == "summarize":
101
+ _summarize(args.path)
102
+ elif args.cmd == "report":
103
+ _report(args.path)
104
+ elif args.cmd == "view":
105
+ from agentguard.viewer import serve
106
+
107
+ serve(args.path, port=args.port, open_browser=not args.no_open)
108
+ else:
109
+ parser.print_help()
110
+
111
+
112
+ if __name__ == "__main__":
113
+ main()
@@ -0,0 +1,100 @@
1
+ from __future__ import annotations
2
+
3
+ from collections import deque
4
+ from dataclasses import dataclass
5
+ from typing import Any, Deque, Dict, Optional, Tuple
6
+ import json
7
+ import time
8
+
9
+
10
+ class LoopDetected(RuntimeError):
11
+ pass
12
+
13
+
14
+ class BudgetExceeded(RuntimeError):
15
+ pass
16
+
17
+
18
+ class TimeoutExceeded(RuntimeError):
19
+ pass
20
+
21
+
22
+ class LoopGuard:
23
+ def __init__(self, max_repeats: int = 3, window: int = 6) -> None:
24
+ if max_repeats < 2:
25
+ raise ValueError("max_repeats must be >= 2")
26
+ if window < max_repeats:
27
+ raise ValueError("window must be >= max_repeats")
28
+ self._max_repeats = max_repeats
29
+ self._history: Deque[Tuple[str, str]] = deque(maxlen=window)
30
+
31
+ def check(self, tool_name: str, tool_args: Optional[Dict[str, Any]] = None) -> None:
32
+ args = tool_args or {}
33
+ signature = (tool_name, _stable_json(args))
34
+ self._history.append(signature)
35
+ if len(self._history) < self._max_repeats:
36
+ return
37
+ last_n = list(self._history)[-self._max_repeats :]
38
+ if len(set(last_n)) == 1:
39
+ raise LoopDetected(
40
+ f"Detected repeated tool call {tool_name} {self._max_repeats} times"
41
+ )
42
+
43
+ def reset(self) -> None:
44
+ self._history.clear()
45
+
46
+
47
+ @dataclass
48
+ class BudgetState:
49
+ tokens_used: int = 0
50
+ calls_used: int = 0
51
+
52
+
53
+ class BudgetGuard:
54
+ def __init__(self, max_tokens: Optional[int] = None, max_calls: Optional[int] = None) -> None:
55
+ if max_tokens is None and max_calls is None:
56
+ raise ValueError("Provide max_tokens or max_calls")
57
+ self._max_tokens = max_tokens
58
+ self._max_calls = max_calls
59
+ self.state = BudgetState()
60
+
61
+ def consume(self, tokens: int = 0, calls: int = 0) -> None:
62
+ self.state.tokens_used += tokens
63
+ self.state.calls_used += calls
64
+ if self._max_tokens is not None and self.state.tokens_used > self._max_tokens:
65
+ raise BudgetExceeded(
66
+ f"Token budget exceeded: {self.state.tokens_used} > {self._max_tokens}"
67
+ )
68
+ if self._max_calls is not None and self.state.calls_used > self._max_calls:
69
+ raise BudgetExceeded(
70
+ f"Call budget exceeded: {self.state.calls_used} > {self._max_calls}"
71
+ )
72
+
73
+ def reset(self) -> None:
74
+ self.state = BudgetState()
75
+
76
+
77
+ class TimeoutGuard:
78
+ def __init__(self, max_seconds: float) -> None:
79
+ if max_seconds <= 0:
80
+ raise ValueError("max_seconds must be > 0")
81
+ self._max_seconds = max_seconds
82
+ self._start: Optional[float] = None
83
+
84
+ def start(self) -> None:
85
+ self._start = time.monotonic()
86
+
87
+ def check(self) -> None:
88
+ if self._start is None:
89
+ raise RuntimeError("TimeoutGuard.start() must be called before check()")
90
+ if (time.monotonic() - self._start) > self._max_seconds:
91
+ raise TimeoutExceeded(
92
+ f"Run exceeded {self._max_seconds}s timeout"
93
+ )
94
+
95
+ def reset(self) -> None:
96
+ self._start = None
97
+
98
+
99
+ def _stable_json(data: Dict[str, Any]) -> str:
100
+ return json.dumps(data, sort_keys=True, separators=(",", ":"))
@@ -0,0 +1,5 @@
1
+ """Framework integration stubs."""
2
+
3
+ from .langchain import AgentGuardCallbackHandler
4
+
5
+ __all__ = ["AgentGuardCallbackHandler"]