agent-riggs 0.1.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. agent_riggs/__init__.py +3 -0
  2. agent_riggs/assembly.py +53 -0
  3. agent_riggs/briefing/__init__.py +0 -0
  4. agent_riggs/briefing/project.py +21 -0
  5. agent_riggs/briefing/session.py +84 -0
  6. agent_riggs/cli.py +255 -0
  7. agent_riggs/config.py +103 -0
  8. agent_riggs/defaults/config.toml +35 -0
  9. agent_riggs/ingest/__init__.py +0 -0
  10. agent_riggs/ingest/pipeline.py +141 -0
  11. agent_riggs/ingest/sources/__init__.py +0 -0
  12. agent_riggs/ingest/sources/base.py +18 -0
  13. agent_riggs/ingest/sources/blq.py +18 -0
  14. agent_riggs/ingest/sources/fledgling.py +18 -0
  15. agent_riggs/ingest/sources/jetsam.py +18 -0
  16. agent_riggs/ingest/sources/kibitzer.py +80 -0
  17. agent_riggs/mcp/__init__.py +0 -0
  18. agent_riggs/mcp/server.py +28 -0
  19. agent_riggs/metrics/__init__.py +0 -0
  20. agent_riggs/metrics/compute.py +76 -0
  21. agent_riggs/metrics/trends.py +44 -0
  22. agent_riggs/plugins/__init__.py +0 -0
  23. agent_riggs/plugins/base.py +37 -0
  24. agent_riggs/plugins/briefing.py +40 -0
  25. agent_riggs/plugins/ingest.py +47 -0
  26. agent_riggs/plugins/metrics.py +59 -0
  27. agent_riggs/plugins/ratchet.py +91 -0
  28. agent_riggs/plugins/sandbox.py +51 -0
  29. agent_riggs/plugins/trust.py +163 -0
  30. agent_riggs/ratchet/__init__.py +0 -0
  31. agent_riggs/ratchet/aggregator.py +23 -0
  32. agent_riggs/ratchet/candidates.py +113 -0
  33. agent_riggs/ratchet/history.py +24 -0
  34. agent_riggs/ratchet/promotions.py +33 -0
  35. agent_riggs/sandbox/__init__.py +0 -0
  36. agent_riggs/sandbox/grades.py +27 -0
  37. agent_riggs/sandbox/integration.py +15 -0
  38. agent_riggs/sandbox/recommendations.py +23 -0
  39. agent_riggs/service.py +35 -0
  40. agent_riggs/store.py +38 -0
  41. agent_riggs/trust/__init__.py +0 -0
  42. agent_riggs/trust/events.py +30 -0
  43. agent_riggs/trust/ewma.py +65 -0
  44. agent_riggs/trust/scorer.py +22 -0
  45. agent_riggs/trust/transitions.py +79 -0
  46. agent_riggs-0.1.0.dist-info/METADATA +19 -0
  47. agent_riggs-0.1.0.dist-info/RECORD +49 -0
  48. agent_riggs-0.1.0.dist-info/WHEEL +4 -0
  49. agent_riggs-0.1.0.dist-info/entry_points.txt +2 -0
@@ -0,0 +1,3 @@
1
+ """Agent Riggs — cross-session memory and analysis for the Rigged tool suite."""
2
+
3
+ __version__ = "0.1.0"
@@ -0,0 +1,53 @@
1
+ """Build a RiggsService from discovered plugins."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from pathlib import Path
6
+
7
+ from agent_riggs.config import load_config
8
+ from agent_riggs.service import RiggsService
9
+ from agent_riggs.store import Store
10
+
11
+
12
+ def assemble(project_root: Path, read_only: bool = False) -> RiggsService:
13
+ """Build the service with all discovered plugins."""
14
+ config = load_config(project_root)
15
+ store_path = project_root / config.store.path
16
+ store = Store(store_path, read_only=read_only)
17
+ service = RiggsService(project_root, store, config)
18
+
19
+ _register_core_plugins(service)
20
+ _register_optional_plugins(service)
21
+
22
+ ddl: list[str] = []
23
+ for p in service.plugins.values():
24
+ ddl.extend(p.schema_ddl())
25
+ if not read_only:
26
+ store.ensure_schema(ddl)
27
+
28
+ return service
29
+
30
+
31
+ def _register_core_plugins(service: RiggsService) -> None:
32
+ """Register plugins that are always available."""
33
+ from agent_riggs.plugins.briefing import BriefingPlugin
34
+ from agent_riggs.plugins.ingest import IngestPlugin
35
+ from agent_riggs.plugins.metrics import MetricsPlugin
36
+ from agent_riggs.plugins.ratchet import RatchetPlugin
37
+ from agent_riggs.plugins.trust import TrustPlugin
38
+
39
+ service.register(TrustPlugin())
40
+ service.register(IngestPlugin())
41
+ service.register(RatchetPlugin())
42
+ service.register(MetricsPlugin())
43
+ service.register(BriefingPlugin())
44
+
45
+
46
+ def _register_optional_plugins(service: RiggsService) -> None:
47
+ """Register plugins whose backing tools may not be installed."""
48
+ import shutil
49
+
50
+ if shutil.which("blq"):
51
+ from agent_riggs.plugins.sandbox import SandboxPlugin
52
+
53
+ service.register(SandboxPlugin())
File without changes
@@ -0,0 +1,21 @@
1
+ """Project-level health assessment."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Any
6
+
7
+ from agent_riggs.store import Store
8
+
9
+
10
+ def project_health(store: Store, project: str) -> dict[str, Any]:
11
+ row = store.execute(
12
+ """SELECT count(*), coalesce(avg(trust_end), 1.0), coalesce(avg(failure_rate), 0.0)
13
+ FROM session_summaries WHERE project = ?""",
14
+ [project],
15
+ ).fetchone()
16
+ return {
17
+ "total_sessions": row[0],
18
+ "avg_trust": row[1],
19
+ "avg_failure_rate": row[2],
20
+ "healthy": row[1] >= 0.7 and row[2] <= 0.3,
21
+ }
@@ -0,0 +1,84 @@
1
+ """Generate session briefings from cross-session data."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from dataclasses import dataclass
6
+ from typing import Any
7
+
8
+ from agent_riggs.config import RiggsConfig
9
+ from agent_riggs.store import Store
10
+
11
+
12
+ @dataclass
13
+ class SessionBriefing:
14
+ trust_baseline: float | None
15
+ last_session: dict[str, Any] | None
16
+ known_issues: list[str]
17
+ active_candidates: int
18
+
19
+ def format(self):
20
+ lines = []
21
+ if self.trust_baseline is not None:
22
+ lines.append(f"Trust baseline: {self.trust_baseline:.2f}")
23
+ else:
24
+ lines.append("Trust baseline: no data")
25
+ if self.last_session:
26
+ s = self.last_session
27
+ lines.append(
28
+ f"Last session: {s['session_id']}, {s['total_turns']} turns, "
29
+ f"trust {s['trust_end']:.2f}"
30
+ )
31
+ else:
32
+ lines.append("Last session: none")
33
+ if self.known_issues:
34
+ lines.append("\nKnown issues:")
35
+ for issue in self.known_issues:
36
+ lines.append(f" - {issue}")
37
+ if self.active_candidates > 0:
38
+ lines.append(f"\nActive ratchet candidates: {self.active_candidates}")
39
+ return "\n".join(lines)
40
+
41
+
42
+ def generate_briefing(store: Store, project: str, config: RiggsConfig) -> SessionBriefing:
43
+ trust_row = store.execute(
44
+ "SELECT trust_15 FROM turns WHERE project = ? ORDER BY timestamp DESC LIMIT 1",
45
+ [project],
46
+ ).fetchone()
47
+ trust_baseline = trust_row[0] if trust_row else None
48
+
49
+ session_row = store.execute(
50
+ """SELECT session_id, total_turns, total_failures, trust_end, CAST(ended_at AS VARCHAR)
51
+ FROM session_summaries WHERE project = ? ORDER BY ended_at DESC LIMIT 1""",
52
+ [project],
53
+ ).fetchone()
54
+ last_session = None
55
+ if session_row:
56
+ last_session = {
57
+ "session_id": session_row[0],
58
+ "total_turns": session_row[1],
59
+ "total_failures": session_row[2],
60
+ "trust_end": session_row[3],
61
+ "ended_at": session_row[4],
62
+ }
63
+
64
+ failure_rows = store.execute(
65
+ """SELECT failure_category, count(*) AS cnt FROM failure_stream
66
+ WHERE project = ? GROUP BY failure_category
67
+ HAVING count(*) >= 3 ORDER BY cnt DESC LIMIT 5""",
68
+ [project],
69
+ ).fetchall()
70
+ known_issues = [f"{r[0]} ({r[1]} occurrences)" for r in failure_rows]
71
+
72
+ candidate_row = store.execute(
73
+ """SELECT count(DISTINCT failure_category || coalesce(tool_name, '') || coalesce(mode, ''))
74
+ FROM failure_stream WHERE project = ? GROUP BY project HAVING count(*) >= ?""",
75
+ [project, config.ratchet.min_frequency],
76
+ ).fetchone()
77
+ active_candidates = candidate_row[0] if candidate_row else 0
78
+
79
+ return SessionBriefing(
80
+ trust_baseline=trust_baseline,
81
+ last_session=last_session,
82
+ known_issues=known_issues,
83
+ active_candidates=active_candidates,
84
+ )
agent_riggs/cli.py ADDED
@@ -0,0 +1,255 @@
1
+ """CLI entry point for agent-riggs."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import shutil
6
+ from importlib import resources
7
+ from pathlib import Path
8
+
9
+ import click
10
+
11
+ from agent_riggs import __version__
12
+
13
+
14
+ def find_project_root() -> Path:
15
+ """Find the project root (cwd for now)."""
16
+ return Path.cwd()
17
+
18
+
19
+ @click.group()
20
+ @click.version_option(__version__, prog_name="agent-riggs")
21
+ @click.pass_context
22
+ def main(ctx: click.Context) -> None:
23
+ """agent-riggs -- cross-session memory and analysis for the Rigged tool suite."""
24
+ ctx.ensure_object(dict)
25
+
26
+
27
+ @main.command()
28
+ def init() -> None:
29
+ """Initialize .riggs/ directory and store."""
30
+ project_root = find_project_root()
31
+ riggs_dir = project_root / ".riggs"
32
+ riggs_dir.mkdir(exist_ok=True)
33
+
34
+ config_path = riggs_dir / "config.toml"
35
+ if not config_path.exists():
36
+ defaults_ref = resources.files("agent_riggs") / "defaults" / "config.toml"
37
+ config_path.write_text(defaults_ref.read_text(encoding="utf-8"))
38
+
39
+ from agent_riggs.assembly import assemble
40
+
41
+ service = assemble(project_root)
42
+
43
+ tools = []
44
+ for name in ("kibitzer", "blq", "jetsam", "fledgling"):
45
+ if shutil.which(name) or (project_root / f".{name}").exists():
46
+ tools.append(name)
47
+
48
+ click.echo(f"Initialized .riggs/ in {project_root}")
49
+ if tools:
50
+ click.echo(f"Discovered tools: {', '.join(tools)}")
51
+ else:
52
+ click.echo("No sibling tools discovered yet.")
53
+
54
+ service.store.close()
55
+
56
+
57
+ @main.command()
58
+ def ingest() -> None:
59
+ """Ingest session data from sibling tools."""
60
+ from agent_riggs.assembly import assemble
61
+
62
+ project_root = find_project_root()
63
+ service = assemble(project_root)
64
+
65
+ ingest_plugin = service.plugin("ingest")
66
+ result = ingest_plugin.run()
67
+
68
+ click.echo(f"Ingested {result.turns_ingested} turns from {result.sources_read}")
69
+ if result.failures_recorded:
70
+ click.echo(f"Recorded {result.failures_recorded} failures")
71
+
72
+ service.store.close()
73
+
74
+
75
+ @main.command()
76
+ def status() -> None:
77
+ """Show trust scores, mode, and ratchet summary."""
78
+ from agent_riggs.assembly import assemble
79
+
80
+ project_root = find_project_root()
81
+ service = assemble(project_root)
82
+
83
+ trust_plugin = service.plugin("trust")
84
+ data = trust_plugin.current()
85
+
86
+ if not data["has_data"]:
87
+ click.echo("trust: no data yet")
88
+ click.echo("\nRun `agent-riggs ingest` after a session.")
89
+ else:
90
+ click.echo(
91
+ f"trust: {data['trust_1']:.2f} / {data['trust_5']:.2f} / {data['trust_15']:.2f}"
92
+ )
93
+ click.echo(" now session baseline")
94
+
95
+ service.store.close()
96
+
97
+
98
+ # --- Trust commands ---
99
+
100
+
101
+ @main.group("trust")
102
+ def trust_group() -> None:
103
+ """Trust score commands."""
104
+
105
+
106
+ @trust_group.command("current")
107
+ def trust_current() -> None:
108
+ """Show current trust scores."""
109
+ from agent_riggs.assembly import assemble
110
+
111
+ service = assemble(find_project_root())
112
+ data = service.plugin("trust").current()
113
+ if not data["has_data"]:
114
+ click.echo("No trust data yet.")
115
+ else:
116
+ click.echo(f"trust_1 (now): {data['trust_1']:.4f}")
117
+ click.echo(f"trust_5 (session): {data['trust_5']:.4f}")
118
+ click.echo(f"trust_15 (baseline):{data['trust_15']:.4f}")
119
+ service.store.close()
120
+
121
+
122
+ @trust_group.command("history")
123
+ @click.option("--limit", default=20, help="Number of entries")
124
+ def trust_history(limit: int) -> None:
125
+ """Show trust score history."""
126
+ from agent_riggs.assembly import assemble
127
+
128
+ service = assemble(find_project_root())
129
+ history = service.plugin("trust").history(limit=limit)
130
+ if not history:
131
+ click.echo("No trust history.")
132
+ else:
133
+ for entry in history:
134
+ click.echo(
135
+ f" [{entry['session_id']}:{entry['turn_number']}] "
136
+ f"{entry['trust_1']:.2f} / {entry['trust_5']:.2f} / {entry['trust_15']:.2f}"
137
+ )
138
+ service.store.close()
139
+
140
+
141
+ # --- Ratchet commands ---
142
+
143
+
144
+ @main.group("ratchet")
145
+ def ratchet_group() -> None:
146
+ """Ratchet candidate commands."""
147
+
148
+
149
+ @ratchet_group.command("candidates")
150
+ def ratchet_candidates() -> None:
151
+ """Show pending ratchet candidates."""
152
+ from agent_riggs.assembly import assemble
153
+
154
+ service = assemble(find_project_root())
155
+ candidates = service.plugin("ratchet").candidates()
156
+ if not candidates:
157
+ click.echo("No ratchet candidates.")
158
+ else:
159
+ for c in candidates:
160
+ click.echo(f"\n [{c.candidate_type}] {c.candidate_key}")
161
+ click.echo(f" {c.recommendation}")
162
+ click.echo(f" Evidence: {c.evidence}")
163
+ service.store.close()
164
+
165
+
166
+ @ratchet_group.command("promote")
167
+ @click.argument("key")
168
+ @click.option("--reason", default=None, help="Reason for promotion")
169
+ def ratchet_promote(key: str, reason: str | None) -> None:
170
+ """Promote a ratchet candidate."""
171
+ from agent_riggs.assembly import assemble
172
+
173
+ service = assemble(find_project_root())
174
+ try:
175
+ service.plugin("ratchet").promote(key, reason)
176
+ click.echo(f"Promoted: {key}")
177
+ except KeyError:
178
+ click.echo(f"No candidate with key: {key}", err=True)
179
+ service.store.close()
180
+
181
+
182
+ @ratchet_group.command("reject")
183
+ @click.argument("key")
184
+ @click.option("--reason", required=True, help="Reason for rejection")
185
+ def ratchet_reject(key: str, reason: str) -> None:
186
+ """Reject a ratchet candidate with reason."""
187
+ from agent_riggs.assembly import assemble
188
+
189
+ service = assemble(find_project_root())
190
+ try:
191
+ service.plugin("ratchet").reject(key, reason)
192
+ click.echo(f"Rejected: {key}")
193
+ except KeyError:
194
+ click.echo(f"No candidate with key: {key}", err=True)
195
+ service.store.close()
196
+
197
+
198
+ @ratchet_group.command("history")
199
+ def ratchet_history() -> None:
200
+ """Show ratchet decision history."""
201
+ from agent_riggs.assembly import assemble
202
+
203
+ service = assemble(find_project_root())
204
+ history = service.plugin("ratchet").history()
205
+ if not history:
206
+ click.echo("No ratchet decisions recorded.")
207
+ else:
208
+ for h in history:
209
+ click.echo(
210
+ f" [{h['decided_at']}] {h['decision']}: "
211
+ f"{h['candidate_key']} — {h.get('reason', '')}"
212
+ )
213
+ service.store.close()
214
+
215
+
216
+ # --- Metrics command ---
217
+
218
+
219
+ @main.command("metrics")
220
+ @click.option("--period", default=None, type=int, help="Period in days")
221
+ def metrics_cmd(period: int | None) -> None:
222
+ """Show ratchet metrics dashboard."""
223
+ from agent_riggs.assembly import assemble
224
+
225
+ service = assemble(find_project_root())
226
+ m = service.plugin("metrics").compute(period)
227
+ click.echo(f"RATCHET METRICS ({m.total_sessions} sessions)\n")
228
+ click.echo(f" Ratchet velocity: {m.ratchet_velocity} promotions")
229
+ click.echo(f" Self-service ratio: {m.structured_tool_fraction:.0%} structured")
230
+ click.echo(f" Computation channel %: {m.computation_channel_fraction:.0%}")
231
+ click.echo(
232
+ f" Trust trajectory: {m.trust_trajectory_start:.2f} -> "
233
+ f"{m.trust_trajectory_end:.2f}"
234
+ )
235
+ click.echo(f" Failure rate: {m.failure_rate:.0%}")
236
+ if m.mode_distribution:
237
+ click.echo("\n Mode distribution:")
238
+ for mode, frac in sorted(m.mode_distribution.items(), key=lambda x: -x[1]):
239
+ click.echo(f" {mode}: {frac:.0%}")
240
+ service.store.close()
241
+
242
+
243
+ # --- Brief command ---
244
+
245
+
246
+ @main.command("brief")
247
+ def brief_cmd() -> None:
248
+ """Full session briefing."""
249
+ from agent_riggs.assembly import assemble
250
+
251
+ service = assemble(find_project_root())
252
+ briefing = service.plugin("briefing").brief()
253
+ click.echo(f"PROJECT BRIEFING: {find_project_root().name}\n")
254
+ click.echo(briefing.format())
255
+ service.store.close()
agent_riggs/config.py ADDED
@@ -0,0 +1,103 @@
1
+ """Configuration loading: merge defaults with .riggs/config.toml."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import tomllib
6
+ from dataclasses import dataclass, field, fields
7
+ from importlib import resources
8
+ from pathlib import Path
9
+ from typing import Any
10
+
11
+
12
+ @dataclass
13
+ class TrustConfig:
14
+ score_success: float = 1.0
15
+ score_suboptimal: float = 0.7
16
+ score_mode_switch_agent: float = 0.8
17
+ score_mode_switch_controller: float = 0.3
18
+ score_failure: float = 0.2
19
+ score_path_denial: float = 0.1
20
+ score_repeated_failure: float = 0.0
21
+ alpha_short: float = 0.4
22
+ alpha_session: float = 0.08
23
+ alpha_baseline: float = 0.02
24
+ tighten_threshold: float = 0.3
25
+ auto_tighten_threshold: float = 0.5
26
+ loosen_threshold: float = 0.9
27
+ loosen_sustained_turns: int = 20
28
+
29
+
30
+ @dataclass
31
+ class RatchetConfig:
32
+ min_frequency: int = 5
33
+ min_sessions: int = 3
34
+ min_success_rate: float = 0.8
35
+ lookback_days: int = 30
36
+
37
+
38
+ @dataclass
39
+ class SandboxConfig:
40
+ memory_headroom: float = 2.0
41
+ timeout_headroom: float = 3.0
42
+ cpu_headroom: float = 2.0
43
+ min_runs_for_tightening: int = 5
44
+
45
+
46
+ @dataclass
47
+ class MetricsConfig:
48
+ default_period_days: int = 30
49
+
50
+
51
+ @dataclass
52
+ class StoreConfig:
53
+ path: str = ".riggs/store.duckdb"
54
+
55
+
56
+ @dataclass
57
+ class RiggsConfig:
58
+ trust: TrustConfig = field(default_factory=TrustConfig)
59
+ ratchet: RatchetConfig = field(default_factory=RatchetConfig)
60
+ sandbox: SandboxConfig = field(default_factory=SandboxConfig)
61
+ metrics: MetricsConfig = field(default_factory=MetricsConfig)
62
+ store: StoreConfig = field(default_factory=StoreConfig)
63
+
64
+
65
+ def _load_defaults() -> dict[str, Any]:
66
+ """Load the shipped defaults/config.toml."""
67
+ defaults_ref = resources.files("agent_riggs") / "defaults" / "config.toml"
68
+ return tomllib.loads(defaults_ref.read_text(encoding="utf-8"))
69
+
70
+
71
+ def _deep_merge(base: dict[str, Any], override: dict[str, Any]) -> dict[str, Any]:
72
+ """Merge override into base, recursing into nested dicts."""
73
+ merged = dict(base)
74
+ for key, value in override.items():
75
+ if key in merged and isinstance(merged[key], dict) and isinstance(value, dict):
76
+ merged[key] = _deep_merge(merged[key], value)
77
+ else:
78
+ merged[key] = value
79
+ return merged
80
+
81
+
82
+ def _dict_to_dataclass(cls: type, data: dict[str, Any]) -> Any:
83
+ """Convert a dict to a dataclass, ignoring unknown keys."""
84
+ known = {f.name for f in fields(cls)}
85
+ return cls(**{k: v for k, v in data.items() if k in known})
86
+
87
+
88
+ def load_config(project_root: Path) -> RiggsConfig:
89
+ """Load config: defaults merged with .riggs/config.toml."""
90
+ merged = _load_defaults()
91
+
92
+ user_path = project_root / ".riggs" / "config.toml"
93
+ if user_path.exists():
94
+ user = tomllib.loads(user_path.read_text(encoding="utf-8"))
95
+ merged = _deep_merge(merged, user)
96
+
97
+ return RiggsConfig(
98
+ trust=_dict_to_dataclass(TrustConfig, merged.get("trust", {})),
99
+ ratchet=_dict_to_dataclass(RatchetConfig, merged.get("ratchet", {})),
100
+ sandbox=_dict_to_dataclass(SandboxConfig, merged.get("sandbox", {})),
101
+ metrics=_dict_to_dataclass(MetricsConfig, merged.get("metrics", {})),
102
+ store=_dict_to_dataclass(StoreConfig, merged.get("store", {})),
103
+ )
@@ -0,0 +1,35 @@
1
+ [trust]
2
+ score_success = 1.0
3
+ score_suboptimal = 0.7
4
+ score_mode_switch_agent = 0.8
5
+ score_mode_switch_controller = 0.3
6
+ score_failure = 0.2
7
+ score_path_denial = 0.1
8
+ score_repeated_failure = 0.0
9
+
10
+ alpha_short = 0.4
11
+ alpha_session = 0.08
12
+ alpha_baseline = 0.02
13
+
14
+ tighten_threshold = 0.3
15
+ auto_tighten_threshold = 0.5
16
+ loosen_threshold = 0.9
17
+ loosen_sustained_turns = 20
18
+
19
+ [ratchet]
20
+ min_frequency = 5
21
+ min_sessions = 3
22
+ min_success_rate = 0.8
23
+ lookback_days = 30
24
+
25
+ [sandbox]
26
+ memory_headroom = 2.0
27
+ timeout_headroom = 3.0
28
+ cpu_headroom = 2.0
29
+ min_runs_for_tightening = 5
30
+
31
+ [metrics]
32
+ default_period_days = 30
33
+
34
+ [store]
35
+ path = ".riggs/store.duckdb"
File without changes