agentsam-sdk 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.
- agentsam_sdk/__init__.py +13 -0
- agentsam_sdk/cli.py +263 -0
- agentsam_sdk/client.py +220 -0
- agentsam_sdk/data/__init__.py +0 -0
- agentsam_sdk/data/agentsam_walk.py +158 -0
- agentsam_sdk/data/d1_adapter.py +124 -0
- agentsam_sdk/data/d1_bloat.py +445 -0
- agentsam_sdk/errors/__init__.py +4 -0
- agentsam_sdk/errors/vocabulary.py +2740 -0
- agentsam_sdk/hooks/__init__.py +32 -0
- agentsam_sdk/hooks/adapters.py +132 -0
- agentsam_sdk/hooks/contracts.py +121 -0
- agentsam_sdk/hooks/runtime.py +253 -0
- agentsam_sdk/knowledge/__init__.py +3 -0
- agentsam_sdk/knowledge/client.py +33 -0
- agentsam_sdk/knowledge/models.py +126 -0
- agentsam_sdk/py.typed +0 -0
- agentsam_sdk/repository/__init__.py +12 -0
- agentsam_sdk/repository/__main__.py +3 -0
- agentsam_sdk/repository/inspect.py +496 -0
- agentsam_sdk/repository/intelligence/__init__.py +5 -0
- agentsam_sdk/repository/intelligence/__main__.py +3 -0
- agentsam_sdk/repository/intelligence/cli.py +84 -0
- agentsam_sdk/repository/intelligence/discovery.py +153 -0
- agentsam_sdk/repository/intelligence/git.py +84 -0
- agentsam_sdk/repository/intelligence/metrics.py +92 -0
- agentsam_sdk/repository/intelligence/snapshot.py +108 -0
- agentsam_sdk/repository/inventory.py +352 -0
- agentsam_sdk/repository/recon/__init__.py +28 -0
- agentsam_sdk/repository/recon/__main__.py +3 -0
- agentsam_sdk/repository/recon/cli.py +178 -0
- agentsam_sdk/repository/recon/packet.py +294 -0
- agentsam_sdk/repository/recon/validate.py +76 -0
- agentsam_sdk/repository/scan_bloat.py +173 -0
- agentsam_sdk/runtime/__init__.py +0 -0
- agentsam_sdk/runtime/contract.py +119 -0
- agentsam_sdk/tui/__init__.py +52 -0
- agentsam_sdk/tui/__main__.py +4 -0
- agentsam_sdk/tui/bootstrap.py +15 -0
- agentsam_sdk/tui/demo.py +306 -0
- agentsam_sdk/tui/frames.py +147 -0
- agentsam_sdk/tui/onboarding.py +208 -0
- agentsam_sdk/tui/status.py +87 -0
- agentsam_sdk/tui/theme.py +57 -0
- agentsam_sdk/tui/widgets.py +218 -0
- agentsam_sdk-0.1.0.dist-info/METADATA +107 -0
- agentsam_sdk-0.1.0.dist-info/RECORD +51 -0
- agentsam_sdk-0.1.0.dist-info/WHEEL +5 -0
- agentsam_sdk-0.1.0.dist-info/entry_points.txt +2 -0
- agentsam_sdk-0.1.0.dist-info/licenses/LICENSE +21 -0
- agentsam_sdk-0.1.0.dist-info/top_level.txt +1 -0
agentsam_sdk/__init__.py
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
"""agentsam_sdk — stdlib-only toolkit for inneranimalmedia D1/repo audits.
|
|
2
|
+
|
|
3
|
+
Every tool follows the runtime.contract pattern: ToolInput in, ToolResult out,
|
|
4
|
+
plus a receipt written to output_dir for audit trail. No hardcoded
|
|
5
|
+
identity/repo/workspace/tenant values anywhere in this package — see
|
|
6
|
+
runtime.contract.HARD_LAW_NOTE.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
__version__ = "0.1.0"
|
|
10
|
+
|
|
11
|
+
# Public local SAM client. Remote ACP, MCP and app-specific authorities remain
|
|
12
|
+
# separately scoped; their mere existence does not grant this client access.
|
|
13
|
+
from .client import AgentSamClient, AsyncAgentSamClient, Choose, Check, Score, SamClientError
|
agentsam_sdk/cli.py
ADDED
|
@@ -0,0 +1,263 @@
|
|
|
1
|
+
"""agentsam CLI -- stdlib argparse only (no click/typer dep, per project
|
|
2
|
+
convention: Python tooling here is stdlib-only).
|
|
3
|
+
|
|
4
|
+
agentsam data d1-bloat --quick --format markdown --output-dir /tmp/d1-bloat
|
|
5
|
+
agentsam data agentsam-walk --prefix agentsam_ --output-dir /tmp/walk
|
|
6
|
+
agentsam repository inventory --repo-root .. --output-dir /tmp/scan --format json
|
|
7
|
+
agentsam repository inventory --repo-root .. --format json | jq '.data.totals'
|
|
8
|
+
agentsam repository scan-bloat --root src --min-kb 10 --top 30 --format json
|
|
9
|
+
agentsam repository inspect --repo-root . --json --dupes | jq '.duplicates'
|
|
10
|
+
"""
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
import argparse
|
|
14
|
+
import json
|
|
15
|
+
import sys
|
|
16
|
+
|
|
17
|
+
from agentsam_sdk.runtime.contract import ToolInput, ToolResult
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def _print_result(result: ToolResult, fmt: str) -> None:
|
|
21
|
+
if fmt == "json":
|
|
22
|
+
print(json.dumps(result.to_dict(), indent=2, default=str))
|
|
23
|
+
else:
|
|
24
|
+
status = "OK" if result.ok else "FAIL"
|
|
25
|
+
print(f"[{status}] {result.tool} ({result.mode}) — {result.summary}", file=sys.stderr)
|
|
26
|
+
if result.error:
|
|
27
|
+
print(f" error: {result.error}", file=sys.stderr)
|
|
28
|
+
for a in result.artifacts:
|
|
29
|
+
print(f" wrote: {a}", file=sys.stderr)
|
|
30
|
+
# When markdown artifacts were written, surface path; else dump summary data keys
|
|
31
|
+
if fmt == "markdown" and result.ok and result.data.get("categories"):
|
|
32
|
+
# stdout stays quiet when artifacts exist; callers use the .md file
|
|
33
|
+
if not result.artifacts:
|
|
34
|
+
from agentsam_sdk.repository.inventory import _markdown
|
|
35
|
+
|
|
36
|
+
sys.stdout.write(_markdown(result.data))
|
|
37
|
+
if fmt == "markdown" and result.ok and result.data.get("files") is not None:
|
|
38
|
+
from agentsam_sdk.repository.scan_bloat import human_table
|
|
39
|
+
|
|
40
|
+
sys.stdout.write(
|
|
41
|
+
human_table(
|
|
42
|
+
result.data.get("files") or [],
|
|
43
|
+
scanned=int(result.data.get("file_count") or 0),
|
|
44
|
+
total_kb=float(result.data.get("total_kb") or 0),
|
|
45
|
+
total_tokens=int(result.data.get("total_est_tokens") or 0),
|
|
46
|
+
)
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def _cmd_data_d1_bloat(args: argparse.Namespace) -> int:
|
|
51
|
+
from agentsam_sdk.data import d1_bloat
|
|
52
|
+
|
|
53
|
+
mode = "full" if args.full else "quick"
|
|
54
|
+
ti = ToolInput(
|
|
55
|
+
mode=mode,
|
|
56
|
+
params={
|
|
57
|
+
"db": args.db, "config": args.config, "repo_root": args.repo_root,
|
|
58
|
+
"prefix": args.prefix, "workers": args.workers, "top": args.top,
|
|
59
|
+
},
|
|
60
|
+
output_dir=args.output_dir,
|
|
61
|
+
)
|
|
62
|
+
result = d1_bloat.run(ti)
|
|
63
|
+
_print_result(result, args.format)
|
|
64
|
+
return 0 if result.ok else 1
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def _cmd_data_agentsam_walk(args: argparse.Namespace) -> int:
|
|
68
|
+
from agentsam_sdk.data import agentsam_walk
|
|
69
|
+
|
|
70
|
+
ti = ToolInput(
|
|
71
|
+
mode="default",
|
|
72
|
+
params={"db": args.db, "config": args.config, "repo_root": args.repo_root, "prefix": args.prefix},
|
|
73
|
+
output_dir=args.output_dir,
|
|
74
|
+
)
|
|
75
|
+
result = agentsam_walk.run(ti)
|
|
76
|
+
_print_result(result, args.format)
|
|
77
|
+
return 0 if result.ok else 1
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def _cmd_repository_inventory(args: argparse.Namespace) -> int:
|
|
81
|
+
from agentsam_sdk.repository import inventory
|
|
82
|
+
|
|
83
|
+
ti = ToolInput(
|
|
84
|
+
mode="read-only",
|
|
85
|
+
params={
|
|
86
|
+
"repo_root": args.repo_root,
|
|
87
|
+
"top": args.top,
|
|
88
|
+
"min_bytes": args.min_bytes,
|
|
89
|
+
"by_ext": args.by_ext,
|
|
90
|
+
"include_node_modules": args.include_node_modules,
|
|
91
|
+
"include_venvs": args.include_venvs,
|
|
92
|
+
"include_git": args.include_git,
|
|
93
|
+
"include_dist": args.include_dist,
|
|
94
|
+
"follow_symlinks": args.follow_symlinks,
|
|
95
|
+
},
|
|
96
|
+
output_dir=args.output_dir,
|
|
97
|
+
)
|
|
98
|
+
result = inventory.run(ti)
|
|
99
|
+
_print_result(result, args.format)
|
|
100
|
+
return 0 if result.ok else 1
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def _cmd_repository_scan_bloat(args: argparse.Namespace) -> int:
|
|
104
|
+
from agentsam_sdk.repository import scan_bloat
|
|
105
|
+
|
|
106
|
+
ti = ToolInput(
|
|
107
|
+
mode="read-only",
|
|
108
|
+
params={
|
|
109
|
+
"root": args.root,
|
|
110
|
+
"top": args.top,
|
|
111
|
+
"min_kb": args.min_kb,
|
|
112
|
+
"ext": args.ext,
|
|
113
|
+
"exclude": args.exclude,
|
|
114
|
+
},
|
|
115
|
+
output_dir=args.output_dir,
|
|
116
|
+
)
|
|
117
|
+
result = scan_bloat.run(ti)
|
|
118
|
+
# Agent capture: --json-envelope prints data payload only (legacy tools/scan_bloat.py)
|
|
119
|
+
if args.json_envelope:
|
|
120
|
+
print(json.dumps(result.data if result.ok else {"ok": False, "error": result.error}, indent=2))
|
|
121
|
+
return 0 if result.ok else 1
|
|
122
|
+
_print_result(result, args.format)
|
|
123
|
+
return 0 if result.ok else 1
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def _cmd_repository_inspect(args: argparse.Namespace) -> int:
|
|
127
|
+
from agentsam_sdk.repository import inspect as repo_inspect
|
|
128
|
+
|
|
129
|
+
argv: list[str] = []
|
|
130
|
+
if args.repo_root:
|
|
131
|
+
argv.extend(["--repo-root", args.repo_root])
|
|
132
|
+
want_text = bool(args.text) or args.format in ("text", "markdown")
|
|
133
|
+
want_json = bool(args.json) or args.format == "json" or not want_text
|
|
134
|
+
if want_json and not want_text:
|
|
135
|
+
argv.append("--json")
|
|
136
|
+
if want_text and not want_json:
|
|
137
|
+
argv.append("--text")
|
|
138
|
+
if want_text and want_json:
|
|
139
|
+
# Explicit both → JSON wins (machine default) unless only --text
|
|
140
|
+
argv.append("--json")
|
|
141
|
+
if args.dupes:
|
|
142
|
+
argv.append("--dupes")
|
|
143
|
+
if args.all:
|
|
144
|
+
argv.append("--all")
|
|
145
|
+
if args.since:
|
|
146
|
+
argv.extend(["--since", args.since])
|
|
147
|
+
if args.recent is not None:
|
|
148
|
+
argv.extend(["--recent", str(args.recent)])
|
|
149
|
+
if args.largest is not None:
|
|
150
|
+
argv.extend(["--largest", str(args.largest)])
|
|
151
|
+
if args.out:
|
|
152
|
+
argv.extend(["--out", args.out])
|
|
153
|
+
return repo_inspect.main_cli(argv)
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
def build_parser() -> argparse.ArgumentParser:
|
|
157
|
+
ap = argparse.ArgumentParser(prog="agentsam", description="agentsam_sdk CLI")
|
|
158
|
+
sub = ap.add_subparsers(dest="group", required=True)
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
data = sub.add_parser("data", help="D1 data audits")
|
|
162
|
+
data_sub = data.add_subparsers(dest="cmd", required=True)
|
|
163
|
+
|
|
164
|
+
bloat = data_sub.add_parser(
|
|
165
|
+
"d1-bloat",
|
|
166
|
+
help="database-scoped D1 audit (--quick=counts, --full=text sizes)",
|
|
167
|
+
)
|
|
168
|
+
bloat.add_argument("--db", help="D1 database name (else AGENTSAM_D1_DB_NAME)")
|
|
169
|
+
bloat.add_argument("--config", help="wrangler config path (else AGENTSAM_WRANGLER_CONFIG)")
|
|
170
|
+
bloat.add_argument("--repo-root", help="repo root wrangler runs from")
|
|
171
|
+
bloat.add_argument("--quick", action="store_true", default=True,
|
|
172
|
+
help="All tables: COUNT(*) only (default)")
|
|
173
|
+
bloat.add_argument("--full", action="store_true",
|
|
174
|
+
help="All tables: row counts + text/JSON LENGTH estimates")
|
|
175
|
+
bloat.add_argument("--prefix", help="Only tables whose name starts with this prefix")
|
|
176
|
+
bloat.add_argument("--workers", type=int, default=6)
|
|
177
|
+
bloat.add_argument("--top", type=int, default=40)
|
|
178
|
+
bloat.add_argument("--output-dir")
|
|
179
|
+
bloat.add_argument("--format", choices=["json", "markdown"], default="markdown")
|
|
180
|
+
bloat.set_defaults(func=_cmd_data_d1_bloat)
|
|
181
|
+
|
|
182
|
+
walk = data_sub.add_parser("agentsam-walk", help="walk agentsam_* tables")
|
|
183
|
+
walk.add_argument("--db")
|
|
184
|
+
walk.add_argument("--config")
|
|
185
|
+
walk.add_argument("--repo-root")
|
|
186
|
+
walk.add_argument("--prefix", default="agentsam_")
|
|
187
|
+
walk.add_argument("--output-dir")
|
|
188
|
+
walk.add_argument("--format", choices=["json", "markdown"], default="markdown")
|
|
189
|
+
walk.set_defaults(func=_cmd_data_agentsam_walk)
|
|
190
|
+
|
|
191
|
+
repo = sub.add_parser("repository", help="repository-level audits")
|
|
192
|
+
repo_sub = repo.add_subparsers(dest="cmd", required=True)
|
|
193
|
+
inv = repo_sub.add_parser(
|
|
194
|
+
"inventory",
|
|
195
|
+
help="file counts + sizes by logical category (jq-friendly JSON)",
|
|
196
|
+
)
|
|
197
|
+
inv.add_argument("--repo-root", default=".")
|
|
198
|
+
inv.add_argument("--output-dir")
|
|
199
|
+
inv.add_argument("--format", choices=["json", "markdown"], default="json")
|
|
200
|
+
inv.add_argument("--top", type=int, default=20, help="N largest files (0 to omit)")
|
|
201
|
+
inv.add_argument("--min-bytes", type=int, default=0)
|
|
202
|
+
inv.add_argument(
|
|
203
|
+
"--by-ext",
|
|
204
|
+
action=argparse.BooleanOptionalAction,
|
|
205
|
+
default=True,
|
|
206
|
+
help="Include by_extension_detail with byte rollups (default: on)",
|
|
207
|
+
)
|
|
208
|
+
inv.add_argument("--include-node-modules", action="store_true")
|
|
209
|
+
inv.add_argument("--include-venvs", action="store_true")
|
|
210
|
+
inv.add_argument("--include-git", action="store_true")
|
|
211
|
+
inv.add_argument("--include-dist", action="store_true")
|
|
212
|
+
inv.add_argument("--follow-symlinks", action="store_true")
|
|
213
|
+
inv.set_defaults(func=_cmd_repository_inventory)
|
|
214
|
+
|
|
215
|
+
sb = repo_sub.add_parser(
|
|
216
|
+
"scan-bloat",
|
|
217
|
+
help="largest runtime source files (KB/lines/est. tokens)",
|
|
218
|
+
)
|
|
219
|
+
sb.add_argument("--root", default=".", help="directory to scan (default: cwd)")
|
|
220
|
+
sb.add_argument("--top", type=int, default=30)
|
|
221
|
+
sb.add_argument("--min-kb", type=float, default=0)
|
|
222
|
+
sb.add_argument(
|
|
223
|
+
"--ext",
|
|
224
|
+
default=".js,.ts,.jsx,.tsx,.mjs,.cjs",
|
|
225
|
+
help="comma-separated extensions",
|
|
226
|
+
)
|
|
227
|
+
sb.add_argument("--exclude", default="", help="extra dir names to exclude")
|
|
228
|
+
sb.add_argument("--output-dir")
|
|
229
|
+
sb.add_argument("--format", choices=["json", "markdown"], default="markdown")
|
|
230
|
+
sb.add_argument(
|
|
231
|
+
"--json-envelope",
|
|
232
|
+
action="store_true",
|
|
233
|
+
help="print ToolResult.data JSON only (agent/terminal capture)",
|
|
234
|
+
)
|
|
235
|
+
sb.set_defaults(func=_cmd_repository_scan_bloat)
|
|
236
|
+
|
|
237
|
+
insp = repo_sub.add_parser(
|
|
238
|
+
"inspect",
|
|
239
|
+
help="file walk: sizes + dates (+ optional --dupes SHA-256 groups)",
|
|
240
|
+
)
|
|
241
|
+
insp.add_argument("--repo-root", default=None)
|
|
242
|
+
insp.add_argument("--format", choices=["json", "markdown", "text"], default="json")
|
|
243
|
+
insp.add_argument("--json", action="store_true")
|
|
244
|
+
insp.add_argument("--text", action="store_true")
|
|
245
|
+
insp.add_argument("--dupes", action="store_true")
|
|
246
|
+
insp.add_argument("--all", action="store_true")
|
|
247
|
+
insp.add_argument("--since", default=None)
|
|
248
|
+
insp.add_argument("--recent", type=int, default=50)
|
|
249
|
+
insp.add_argument("--largest", type=int, default=30)
|
|
250
|
+
insp.add_argument("--out", default=None)
|
|
251
|
+
insp.set_defaults(func=_cmd_repository_inspect)
|
|
252
|
+
|
|
253
|
+
return ap
|
|
254
|
+
|
|
255
|
+
|
|
256
|
+
def main(argv: list[str] | None = None) -> int:
|
|
257
|
+
parser = build_parser()
|
|
258
|
+
args = parser.parse_args(argv)
|
|
259
|
+
return args.func(args)
|
|
260
|
+
|
|
261
|
+
|
|
262
|
+
if __name__ == "__main__":
|
|
263
|
+
sys.exit(main())
|
agentsam_sdk/client.py
ADDED
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
"""A real, bounded SAM local client: delegates to existing Node machinery.
|
|
2
|
+
|
|
3
|
+
The current Python 0.1 package is a repository/data toolkit, not a remotely
|
|
4
|
+
hosted universal SDK. This bridge exposes read-only deterministic capabilities
|
|
5
|
+
and refuses any write, D1, CMS or arbitrary terminal operation. A native ACP
|
|
6
|
+
client and remote per-installation identity are separately tracked work.
|
|
7
|
+
"""
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import asyncio
|
|
11
|
+
from dataclasses import dataclass
|
|
12
|
+
import json
|
|
13
|
+
import os
|
|
14
|
+
from pathlib import Path
|
|
15
|
+
import shutil
|
|
16
|
+
import subprocess
|
|
17
|
+
import time
|
|
18
|
+
from urllib import request as urllib_request, error as urllib_error
|
|
19
|
+
from typing import Any, Mapping
|
|
20
|
+
|
|
21
|
+
class SamClientError(RuntimeError):
|
|
22
|
+
def __init__(self, code: str):
|
|
23
|
+
self.code = code
|
|
24
|
+
super().__init__(code)
|
|
25
|
+
|
|
26
|
+
@dataclass(frozen=True)
|
|
27
|
+
class Choose:
|
|
28
|
+
options: tuple[str, ...]
|
|
29
|
+
instructions: str = ""
|
|
30
|
+
def __init__(self, options: list[str] | tuple[str,...], instructions: str = ""):
|
|
31
|
+
object.__setattr__(self, "options", tuple(options))
|
|
32
|
+
object.__setattr__(self, "instructions", instructions)
|
|
33
|
+
def question(self) -> dict:
|
|
34
|
+
return {"type":"choose","options":list(self.options),"instructions":self.instructions,"evaluator_hint":"deterministic"}
|
|
35
|
+
|
|
36
|
+
@dataclass(frozen=True)
|
|
37
|
+
class Check:
|
|
38
|
+
instructions: str = ""
|
|
39
|
+
def question(self) -> dict:
|
|
40
|
+
return {"type":"check","instructions":self.instructions,"evaluator_hint":"deterministic"}
|
|
41
|
+
|
|
42
|
+
@dataclass(frozen=True)
|
|
43
|
+
class Score:
|
|
44
|
+
levels: tuple[str, ...]
|
|
45
|
+
instructions: str = ""
|
|
46
|
+
def __init__(self, levels: list[str] | tuple[str,...], instructions: str = ""):
|
|
47
|
+
object.__setattr__(self, "levels", tuple(levels))
|
|
48
|
+
object.__setattr__(self, "instructions", instructions)
|
|
49
|
+
def question(self) -> dict:
|
|
50
|
+
return {"type":"score","levels":list(self.levels),"instructions":self.instructions,"evaluator_hint":"heuristic"}
|
|
51
|
+
|
|
52
|
+
_BRIDGE_REL = Path("src/sam/bridge/stdio.mjs")
|
|
53
|
+
_NPM_PACKAGE = Path("node_modules/@inneranimalmedia/agentsam-sdk")
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def _discover_bridge(cwd: str | Path | None) -> Path | None:
|
|
57
|
+
"""Find the Node bridge without configuration.
|
|
58
|
+
|
|
59
|
+
Order: this tree's own npm/monorepo layout, then the working directory and
|
|
60
|
+
its parents (a checkout, or an installed ``@inneranimalmedia/agentsam-sdk``).
|
|
61
|
+
"""
|
|
62
|
+
roots = [Path(__file__).resolve().parents[2]]
|
|
63
|
+
if cwd is not None:
|
|
64
|
+
start = Path(cwd).expanduser().resolve()
|
|
65
|
+
roots.extend([start, *start.parents])
|
|
66
|
+
for root in roots:
|
|
67
|
+
for candidate in (root / _BRIDGE_REL, root / _NPM_PACKAGE / _BRIDGE_REL):
|
|
68
|
+
if candidate.is_file():
|
|
69
|
+
return candidate
|
|
70
|
+
return None
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def _bridge_path(explicit: str | None, cwd: str | Path | None = None) -> Path:
|
|
74
|
+
candidate = explicit or os.environ.get("AGENTSAM_SDK_JS_BRIDGE")
|
|
75
|
+
path = Path(candidate).expanduser().resolve() if candidate else _discover_bridge(cwd)
|
|
76
|
+
if path is None or not path.is_file():
|
|
77
|
+
raise SamClientError("sam_bridge_not_installed")
|
|
78
|
+
return path
|
|
79
|
+
|
|
80
|
+
def _binary(explicit: str | None) -> str:
|
|
81
|
+
node = explicit or os.environ.get("AGENTSAM_NODE") or shutil.which("node")
|
|
82
|
+
if not node:
|
|
83
|
+
raise SamClientError("sam_node_not_available")
|
|
84
|
+
return node
|
|
85
|
+
|
|
86
|
+
def _normalize_questions(questions: Mapping[str, Any] | list) -> dict | list:
|
|
87
|
+
if isinstance(questions, Mapping):
|
|
88
|
+
return {key:(val.question() if isinstance(val,(Choose,Check,Score)) else val)
|
|
89
|
+
for key,val in questions.items()}
|
|
90
|
+
return questions
|
|
91
|
+
|
|
92
|
+
def _decode(raw: bytes) -> Any:
|
|
93
|
+
try:
|
|
94
|
+
reply = json.loads(raw)
|
|
95
|
+
except (ValueError, TypeError) as exc:
|
|
96
|
+
raise SamClientError("sam_bridge_invalid_response") from exc
|
|
97
|
+
if not isinstance(reply,dict) or not reply.get("ok"):
|
|
98
|
+
raise SamClientError(str((reply.get("error") or {}).get("code","sam_bridge_failed")))
|
|
99
|
+
return reply["result"]
|
|
100
|
+
|
|
101
|
+
class _ControlPlane:
|
|
102
|
+
"""Existing ACP HTTP endpoints, not a second agent-run database."""
|
|
103
|
+
def __init__(self, url: str, token: str):
|
|
104
|
+
if not url.startswith("https://") and not url.startswith("http://127.0.0.1:"):
|
|
105
|
+
raise SamClientError("sam_acp_https_required")
|
|
106
|
+
self.url=url.rstrip("/")
|
|
107
|
+
self.token=token
|
|
108
|
+
def request(self, route: str, payload: dict | None = None) -> dict:
|
|
109
|
+
body=json.dumps(payload).encode() if payload is not None else None
|
|
110
|
+
headers={"Accept":"application/json"}
|
|
111
|
+
if body is not None: headers["Content-Type"]="application/json"
|
|
112
|
+
if self.token: headers["Authorization"]="Bearer "+self.token
|
|
113
|
+
req=urllib_request.Request(self.url+route,data=body,headers=headers,
|
|
114
|
+
method="POST" if body is not None else "GET")
|
|
115
|
+
try:
|
|
116
|
+
with urllib_request.urlopen(req,timeout=20) as response:
|
|
117
|
+
value=json.loads(response.read())
|
|
118
|
+
except urllib_error.HTTPError as exc:
|
|
119
|
+
raise SamClientError("sam_acp_http_"+str(exc.code)) from exc
|
|
120
|
+
except (urllib_error.URLError, TimeoutError) as exc:
|
|
121
|
+
raise SamClientError("sam_acp_unavailable") from exc
|
|
122
|
+
return value
|
|
123
|
+
def start(self, goal: str, **kwargs):
|
|
124
|
+
return self.request("/v1/runs", {"objective":goal,
|
|
125
|
+
"source_client":"agentsam-sdk-python", "surface":kwargs.get("surface","sdk"),
|
|
126
|
+
"metadata":{"requested_operations":kwargs.get("operations",[]),
|
|
127
|
+
"requested_deliverables":kwargs.get("deliverables"),
|
|
128
|
+
"context":kwargs.get("context",{})}})
|
|
129
|
+
def get(self, id: str):
|
|
130
|
+
from urllib.parse import quote
|
|
131
|
+
return self.request("/v1/runs/"+quote(id,safe=""))
|
|
132
|
+
def events(self, id: str, after: int = -1, limit: int = 200):
|
|
133
|
+
from urllib.parse import quote
|
|
134
|
+
return self.request("/v1/runs/"+quote(id,safe="")+"/events?after="+str(int(after))+"&limit="+str(int(limit)))
|
|
135
|
+
|
|
136
|
+
class RunHandle:
|
|
137
|
+
def __init__(self, cp: _ControlPlane, row: dict):
|
|
138
|
+
self.id=row.get("id") or row.get("run_id")
|
|
139
|
+
if not self.id:raise SamClientError("sam_acp_run_missing_id")
|
|
140
|
+
self.status=row.get("status","unknown")
|
|
141
|
+
self._control=cp
|
|
142
|
+
def get(self):return self._control.get(self.id)
|
|
143
|
+
def events(self, *, after: int = -1, limit: int = 200):
|
|
144
|
+
return self._control.events(self.id,after,limit)
|
|
145
|
+
def wait(self, *, attempts: int = 60, interval: float = 1.0):
|
|
146
|
+
for index in range(attempts):
|
|
147
|
+
state=self.get()
|
|
148
|
+
if state.get("status") in ("completed","failed","cancelled","timed_out"):
|
|
149
|
+
return {"id":self.id,"status":state["status"],"run":state,"deliverables":[],
|
|
150
|
+
"note":"ACP authority must supply real evidence and artifact references."}
|
|
151
|
+
if index+1<attempts:time.sleep(interval)
|
|
152
|
+
raise SamClientError("sam_run_wait_timeout")
|
|
153
|
+
|
|
154
|
+
class AsyncRunHandle:
|
|
155
|
+
def __init__(self, sync: RunHandle):
|
|
156
|
+
self.id=sync.id
|
|
157
|
+
self.status=sync.status
|
|
158
|
+
self._sync=sync
|
|
159
|
+
async def get(self):return await asyncio.to_thread(self._sync.get)
|
|
160
|
+
async def events(self, **kwargs):return await asyncio.to_thread(self._sync.events,**kwargs)
|
|
161
|
+
async def wait(self, **kwargs):return await asyncio.to_thread(self._sync.wait,**kwargs)
|
|
162
|
+
|
|
163
|
+
class AgentSamClient:
|
|
164
|
+
"""Synchronous local machine client; no implicit remote credentials."""
|
|
165
|
+
def __init__(self, *, cwd: str | Path = ".", node: str | None = None, bridge: str | None = None, control_plane_url: str | None = None, control_plane_token: str | None = None):
|
|
166
|
+
self.cwd = str(Path(cwd).resolve())
|
|
167
|
+
self.node = _binary(node)
|
|
168
|
+
self.bridge = str(_bridge_path(bridge, self.cwd))
|
|
169
|
+
self.control_plane = _ControlPlane(control_plane_url,control_plane_token or "") if control_plane_url else None
|
|
170
|
+
def _request(self, method: str, **kwargs) -> Any:
|
|
171
|
+
result = subprocess.run([self.node,self.bridge], input=json.dumps({"method":method,**kwargs}),
|
|
172
|
+
text=True, capture_output=True, cwd=self.cwd, timeout=120, check=False)
|
|
173
|
+
return _decode(result.stdout.encode())
|
|
174
|
+
def discover(self, **query): return self._request("discover",query=query)
|
|
175
|
+
def describe(self, operation_id: str): return self._request("describe",id=operation_id)
|
|
176
|
+
def status(self, operation_id: str): return self._request("status",id=operation_id)
|
|
177
|
+
def invoke(self, operation_id: str, input: dict | None = None):
|
|
178
|
+
return self._request("invoke",id=operation_id,input=input or {})
|
|
179
|
+
def decide(self, *, state: dict, questions: Mapping[str, Any] | list, **kwargs):
|
|
180
|
+
return self._request("decide",input={"state":state,"questions":_normalize_questions(questions),**kwargs})
|
|
181
|
+
def run(self, **kwargs):
|
|
182
|
+
if not self.control_plane: raise SamClientError("sam_python_acp_run_adapter_not_configured")
|
|
183
|
+
goal=kwargs.get("goal","")
|
|
184
|
+
if not isinstance(goal,str) or not goal.strip(): raise SamClientError("sam_run_goal_required")
|
|
185
|
+
result=self.control_plane.start(goal, **{k:v for k,v in kwargs.items() if k!="goal"})
|
|
186
|
+
row=result.get("run",result)
|
|
187
|
+
if not isinstance(row,dict) or not (row.get("id") or row.get("run_id")):raise SamClientError("sam_acp_run_missing_id")
|
|
188
|
+
return RunHandle(self.control_plane,row)
|
|
189
|
+
def events(self, run_id: str, **kwargs):
|
|
190
|
+
if not self.control_plane:raise SamClientError("sam_python_acp_run_adapter_not_configured")
|
|
191
|
+
return self.control_plane.events(run_id,**kwargs)
|
|
192
|
+
def __enter__(self):return self
|
|
193
|
+
def __exit__(self, *_):return False
|
|
194
|
+
|
|
195
|
+
class AsyncAgentSamClient(AgentSamClient):
|
|
196
|
+
"""Async subprocess transport to the same authorized local SAM entrypoint."""
|
|
197
|
+
async def _request_async(self, method: str, **kwargs):
|
|
198
|
+
proc = await asyncio.create_subprocess_exec(self.node,self.bridge,stdin=asyncio.subprocess.PIPE,
|
|
199
|
+
stdout=asyncio.subprocess.PIPE,stderr=asyncio.subprocess.PIPE,cwd=self.cwd)
|
|
200
|
+
try:
|
|
201
|
+
out,_ = await asyncio.wait_for(proc.communicate(json.dumps({"method":method,**kwargs}).encode()),120)
|
|
202
|
+
except asyncio.TimeoutError as exc:
|
|
203
|
+
proc.kill()
|
|
204
|
+
await proc.wait()
|
|
205
|
+
raise SamClientError("sam_bridge_timeout") from exc
|
|
206
|
+
return _decode(out)
|
|
207
|
+
async def discover(self, **query):return await self._request_async("discover",query=query)
|
|
208
|
+
async def describe(self, operation_id: str):return await self._request_async("describe",id=operation_id)
|
|
209
|
+
async def status(self, operation_id: str):return await self._request_async("status",id=operation_id)
|
|
210
|
+
async def invoke(self, operation_id: str, input: dict | None = None):
|
|
211
|
+
return await self._request_async("invoke",id=operation_id,input=input or {})
|
|
212
|
+
async def decide(self, *, state: dict, questions: Mapping[str, Any] | list, **kwargs):
|
|
213
|
+
return await self._request_async("decide",input={"state":state,"questions":_normalize_questions(questions),**kwargs})
|
|
214
|
+
async def run(self, **kwargs):
|
|
215
|
+
handle=await asyncio.to_thread(super().run,**kwargs)
|
|
216
|
+
return AsyncRunHandle(handle)
|
|
217
|
+
async def events(self, run_id: str, **kwargs):
|
|
218
|
+
return await asyncio.to_thread(super().events,run_id,**kwargs)
|
|
219
|
+
async def __aenter__(self):return self
|
|
220
|
+
async def __aexit__(self,*_):return False
|
|
File without changes
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
"""agentsam_sdk.data.agentsam_walk -- condensed port of
|
|
2
|
+
scripts/walk_agentsam_tables.py (+ schema-focus of scripts/d1_schema_audit.py).
|
|
3
|
+
|
|
4
|
+
Walks every `agentsam_%` sqlite_master object: schema, indexes, foreign
|
|
5
|
+
keys, row count, freshness (via *_at / *_at_epoch columns if present), and
|
|
6
|
+
groups tables into capability buckets by name-substring heuristics.
|
|
7
|
+
|
|
8
|
+
Note: this is a condensed re-implementation, not a byte-for-byte port of the
|
|
9
|
+
801-line original -- see docs/gaps.md for what's folded in vs deferred
|
|
10
|
+
(duplicate-detection heuristics, per-feature markdown chunking into db/*.md
|
|
11
|
+
files a la d1_schema_audit.py are deferred to a follow-up pass).
|
|
12
|
+
"""
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
from dataclasses import dataclass, field, asdict
|
|
16
|
+
from datetime import datetime, timezone
|
|
17
|
+
from pathlib import Path
|
|
18
|
+
from typing import Optional
|
|
19
|
+
|
|
20
|
+
from agentsam_sdk.data.d1_adapter import D1Adapter, D1AdapterError
|
|
21
|
+
from agentsam_sdk.runtime.contract import ToolInput, ToolResult, write_receipt, start_timer
|
|
22
|
+
|
|
23
|
+
TOOL_NAME = "data.agentsam_walk"
|
|
24
|
+
|
|
25
|
+
CAPABILITY_RULES: dict[str, list[str]] = {
|
|
26
|
+
"agent_run_spine": ["run", "runs", "execution", "executions", "session", "sessions", "step", "steps", "message", "messages"],
|
|
27
|
+
"model_routing": ["model", "catalog", "routing", "route", "routes", "arm", "arms", "requirement", "provider", "prompt"],
|
|
28
|
+
"tools_commands_mcp": ["tool", "tools", "mcp", "command", "commands", "skill", "skills", "invocation", "chain", "script", "scripts"],
|
|
29
|
+
"workflow_dag": ["workflow", "workflows", "node", "nodes", "edge", "edges", "approval", "approvals", "task", "tasks"],
|
|
30
|
+
"memory_rag": ["memory", "embedding", "vector", "chunk", "chunks", "document", "documents", "rag"],
|
|
31
|
+
"observability": ["log", "logs", "trace", "traces", "metric", "metrics", "health", "analytics", "otlp", "error", "errors"],
|
|
32
|
+
"cms_content": ["cms_", "page", "pages", "content", "nav", "template"],
|
|
33
|
+
"security_policy": ["policy", "policies", "guardrail", "trusted", "origin", "auth", "credential"],
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
@dataclass
|
|
38
|
+
class TableWalk:
|
|
39
|
+
name: str
|
|
40
|
+
columns: list[dict] = field(default_factory=list)
|
|
41
|
+
indexes: list[dict] = field(default_factory=list)
|
|
42
|
+
foreign_keys: list[dict] = field(default_factory=list)
|
|
43
|
+
row_count: int = 0
|
|
44
|
+
has_freshness_col: bool = False
|
|
45
|
+
capability: str = "uncategorized"
|
|
46
|
+
error: Optional[str] = None
|
|
47
|
+
flags: list[str] = field(default_factory=list)
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def _capability_for(table: str) -> str:
|
|
51
|
+
"""Score by total matched-keyword *length*, not match count -- a single
|
|
52
|
+
specific hit ("workflow") should outrank two generic ones ("run", "runs").
|
|
53
|
+
"""
|
|
54
|
+
lname = table.lower()
|
|
55
|
+
best, best_score = "uncategorized", 0
|
|
56
|
+
for cap, keywords in CAPABILITY_RULES.items():
|
|
57
|
+
score = sum(len(kw) for kw in keywords if kw in lname)
|
|
58
|
+
if score > best_score:
|
|
59
|
+
best, best_score = cap, score
|
|
60
|
+
return best
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def _walk_table(adapter: D1Adapter, table: str) -> TableWalk:
|
|
64
|
+
tw = TableWalk(name=table, capability=_capability_for(table))
|
|
65
|
+
try:
|
|
66
|
+
cols = adapter.table_columns(table)
|
|
67
|
+
tw.columns = [{"name": n, "type": t} for n, t in cols]
|
|
68
|
+
tw.indexes = adapter.table_indexes(table)
|
|
69
|
+
tw.foreign_keys = adapter.foreign_keys(table)
|
|
70
|
+
tw.row_count = adapter.row_count(table)
|
|
71
|
+
tw.has_freshness_col = any(
|
|
72
|
+
n.lower() in ("updated_at", "created_at", "updated_at_epoch", "created_at_epoch")
|
|
73
|
+
for n, _ in cols
|
|
74
|
+
)
|
|
75
|
+
if tw.row_count == 0:
|
|
76
|
+
tw.flags.append("empty")
|
|
77
|
+
if not tw.has_freshness_col:
|
|
78
|
+
tw.flags.append("no_freshness_column")
|
|
79
|
+
if not tw.indexes and tw.row_count > 1000:
|
|
80
|
+
tw.flags.append("no_index_high_row_count")
|
|
81
|
+
except D1AdapterError as e:
|
|
82
|
+
tw.error = str(e)[:200]
|
|
83
|
+
except Exception as e: # noqa: BLE001
|
|
84
|
+
tw.error = str(e)[:200]
|
|
85
|
+
return tw
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def _render_markdown(walks: list[TableWalk], prefix: str) -> str:
|
|
89
|
+
lines = [
|
|
90
|
+
f"# agentsam walk -- `{prefix}%` objects",
|
|
91
|
+
"",
|
|
92
|
+
f"- **Generated:** {datetime.now(timezone.utc).strftime('%Y-%m-%d %H:%M UTC')}",
|
|
93
|
+
f"- **Tables walked:** {len(walks)}",
|
|
94
|
+
"",
|
|
95
|
+
"## By capability",
|
|
96
|
+
"",
|
|
97
|
+
]
|
|
98
|
+
by_cap: dict[str, list[TableWalk]] = {}
|
|
99
|
+
for w in walks:
|
|
100
|
+
by_cap.setdefault(w.capability, []).append(w)
|
|
101
|
+
for cap in sorted(by_cap):
|
|
102
|
+
lines.append(f"### {cap} ({len(by_cap[cap])})")
|
|
103
|
+
lines.append("")
|
|
104
|
+
lines.append("| Table | Rows | Indexes | FKs | Flags |")
|
|
105
|
+
lines.append("|-------|------|---------|-----|-------|")
|
|
106
|
+
for w in sorted(by_cap[cap], key=lambda x: x.row_count, reverse=True):
|
|
107
|
+
flags = ", ".join(w.flags) or "-"
|
|
108
|
+
err = f" ⚠ {w.error}" if w.error else ""
|
|
109
|
+
lines.append(f"| `{w.name}` | {w.row_count:,} | {len(w.indexes)} | {len(w.foreign_keys)} | {flags}{err} |")
|
|
110
|
+
lines.append("")
|
|
111
|
+
return "\n".join(lines)
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def run(tool_input: ToolInput) -> ToolResult:
|
|
115
|
+
started = start_timer()
|
|
116
|
+
tool_input.assert_read_only()
|
|
117
|
+
p = tool_input.params
|
|
118
|
+
prefix = p.get("prefix", "agentsam_")
|
|
119
|
+
output_dir = tool_input.output_path()
|
|
120
|
+
|
|
121
|
+
try:
|
|
122
|
+
adapter = D1Adapter.from_env(
|
|
123
|
+
db_name=p.get("db"), wrangler_config=p.get("config"), repo_root=p.get("repo_root")
|
|
124
|
+
)
|
|
125
|
+
tables = adapter.list_tables(like=f"{prefix}%")
|
|
126
|
+
walks = [_walk_table(adapter, t) for t in tables]
|
|
127
|
+
md = _render_markdown(walks, prefix)
|
|
128
|
+
json_payload = {
|
|
129
|
+
"prefix": prefix,
|
|
130
|
+
"database": adapter.db_name,
|
|
131
|
+
"table_count": len(walks),
|
|
132
|
+
"tables": [asdict(w) for w in walks],
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
artifacts: list[str] = []
|
|
136
|
+
if output_dir:
|
|
137
|
+
output_dir.mkdir(parents=True, exist_ok=True)
|
|
138
|
+
(output_dir / "agentsam-walk.md").write_text(md, encoding="utf-8")
|
|
139
|
+
(output_dir / "agentsam-walk.json").write_text(
|
|
140
|
+
__import__("json").dumps(json_payload, indent=2), encoding="utf-8"
|
|
141
|
+
)
|
|
142
|
+
artifacts = [str(output_dir / "agentsam-walk.md"), str(output_dir / "agentsam-walk.json")]
|
|
143
|
+
|
|
144
|
+
result = ToolResult(
|
|
145
|
+
ok=True, tool=TOOL_NAME, mode=tool_input.mode, request_id=tool_input.request_id,
|
|
146
|
+
started_at=started, finished_at=start_timer(),
|
|
147
|
+
summary=f"Walked {len(walks)} `{prefix}%` tables.",
|
|
148
|
+
data=json_payload, artifacts=artifacts,
|
|
149
|
+
)
|
|
150
|
+
except D1AdapterError as e:
|
|
151
|
+
result = ToolResult(
|
|
152
|
+
ok=False, tool=TOOL_NAME, mode=tool_input.mode, request_id=tool_input.request_id,
|
|
153
|
+
started_at=started, finished_at=start_timer(),
|
|
154
|
+
summary="D1 adapter error", error=str(e),
|
|
155
|
+
)
|
|
156
|
+
|
|
157
|
+
write_receipt(result, output_dir)
|
|
158
|
+
return result
|