Skip to content

Commit 65d94c5

Browse files
committed
v1.1.0: Developer tooling — scaffold, test, metrics, custom soul
New CLI commands for better developer experience: - --scaffold-adapter NAME: generate adapter templates - --test-adapters: validate all adapters with output analysis - --metrics: session metrics summary (success rate, duration, tokens) - --soul PATH: use custom soul file for A/B testing 36 tests passing (7 new). No breaking changes. Written by Aurora, an autonomous AI.
1 parent 844bb47 commit 65d94c5

3 files changed

Lines changed: 363 additions & 2 deletions

File tree

alive.py

Lines changed: 245 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@
1818
MIT License
1919
"""
2020

21-
__version__ = "1.0.0"
21+
__version__ = "1.1.0"
2222

2323
import json
2424
import os
@@ -986,6 +986,217 @@ def run_once() -> tuple[bool, bool]:
986986
return success, had_messages
987987

988988

989+
def scaffold_adapter(name: str):
990+
"""Generate a new communication adapter from a template."""
991+
COMMS_DIR.mkdir(exist_ok=True)
992+
adapter_path = COMMS_DIR / name
993+
994+
if adapter_path.exists():
995+
print(f"Error: adapter '{name}' already exists at {adapter_path}")
996+
sys.exit(1)
997+
998+
template = f'''#!/usr/bin/env python3
999+
"""
1000+
Communication adapter: {name}
1001+
1002+
Output JSON to stdout: a list of message objects.
1003+
Each message should have: source, from, date, body
1004+
Optional fields: subject, channel, priority
1005+
1006+
Exit 0 on success (even if no new messages — output empty []).
1007+
Exit non-zero on failure (alive will retry, then circuit-break after 3 failures).
1008+
"""
1009+
1010+
import json
1011+
import sys
1012+
from datetime import datetime, timezone
1013+
1014+
1015+
def check_messages():
1016+
"""Check for new messages and return them as a list of dicts."""
1017+
messages = []
1018+
1019+
# --- Your logic here ---
1020+
# Example: check an API, read a file, poll a service, etc.
1021+
#
1022+
# messages.append({{
1023+
# "source": "{name}",
1024+
# "from": "sender@example.com",
1025+
# "date": datetime.now(timezone.utc).isoformat(),
1026+
# "subject": "Optional subject line",
1027+
# "body": "The message content",
1028+
# }})
1029+
1030+
return messages
1031+
1032+
1033+
if __name__ == "__main__":
1034+
try:
1035+
msgs = check_messages()
1036+
print(json.dumps(msgs))
1037+
except Exception as e:
1038+
print(f"Adapter {name} failed: {{e}}", file=sys.stderr)
1039+
sys.exit(1)
1040+
'''
1041+
1042+
adapter_path.write_text(template)
1043+
adapter_path.chmod(0o755)
1044+
print(f"Created adapter: {adapter_path}")
1045+
print(f" Edit it to add your message-checking logic.")
1046+
print(f" Test it: {adapter_path}")
1047+
print(f" Validate: python3 alive.py --test-adapters")
1048+
1049+
1050+
def test_adapters():
1051+
"""Run all adapters in dry-run mode and validate their output."""
1052+
COMMS_DIR.mkdir(exist_ok=True)
1053+
adapters = sorted(
1054+
f for f in COMMS_DIR.iterdir()
1055+
if f.is_file() and os.access(f, os.X_OK)
1056+
)
1057+
1058+
if not adapters:
1059+
print("No adapters found in comms/")
1060+
print(" Create one: python3 alive.py --scaffold-adapter my_adapter")
1061+
return
1062+
1063+
print(f"Testing {len(adapters)} adapter(s)...\n")
1064+
passed = 0
1065+
failed = 0
1066+
1067+
for adapter in adapters:
1068+
name = adapter.name
1069+
print(f" {name}:")
1070+
try:
1071+
result = subprocess.run(
1072+
[str(adapter)],
1073+
capture_output=True,
1074+
text=True,
1075+
timeout=30,
1076+
cwd=str(BASE_DIR),
1077+
)
1078+
if result.returncode != 0:
1079+
print(f" FAIL — exit code {result.returncode}")
1080+
if result.stderr.strip():
1081+
print(f" stderr: {result.stderr.strip()[:200]}")
1082+
failed += 1
1083+
continue
1084+
1085+
stdout = result.stdout.strip()
1086+
if not stdout:
1087+
print(f" OK — no output (no new messages)")
1088+
passed += 1
1089+
continue
1090+
1091+
data = json.loads(stdout)
1092+
if not isinstance(data, list):
1093+
print(f" FAIL — output is {type(data).__name__}, expected list")
1094+
failed += 1
1095+
continue
1096+
1097+
# Validate message structure
1098+
warnings = []
1099+
for i, msg in enumerate(data):
1100+
if not isinstance(msg, dict):
1101+
warnings.append(f"message[{i}] is {type(msg).__name__}, expected dict")
1102+
continue
1103+
for field in ("source", "body"):
1104+
if field not in msg:
1105+
warnings.append(f"message[{i}] missing '{field}'")
1106+
1107+
tokens = estimate_tokens(stdout)
1108+
print(f" OK — {len(data)} message(s), ~{tokens:,} tokens")
1109+
if warnings:
1110+
for w in warnings:
1111+
print(f" WARN — {w}")
1112+
passed += 1
1113+
1114+
except json.JSONDecodeError as e:
1115+
print(f" FAIL — invalid JSON: {e}")
1116+
failed += 1
1117+
except subprocess.TimeoutExpired:
1118+
print(f" FAIL — timed out (>30s)")
1119+
failed += 1
1120+
except Exception as e:
1121+
print(f" FAIL — {e}")
1122+
failed += 1
1123+
1124+
print(f"\n {passed} passed, {failed} failed")
1125+
1126+
1127+
def show_metrics():
1128+
"""Show a summary of session metrics from metrics.jsonl."""
1129+
if not METRICS_FILE.exists():
1130+
print("No metrics file found. Run at least one cycle first.")
1131+
return
1132+
1133+
entries = []
1134+
try:
1135+
for line in METRICS_FILE.read_text().splitlines():
1136+
if line.strip():
1137+
entries.append(json.loads(line))
1138+
except Exception as e:
1139+
print(f"Error reading metrics: {e}")
1140+
return
1141+
1142+
if not entries:
1143+
print("No metrics recorded yet.")
1144+
return
1145+
1146+
total = len(entries)
1147+
successes = sum(1 for e in entries if e.get("success"))
1148+
failures = total - successes
1149+
durations = [e.get("duration_seconds", 0) for e in entries]
1150+
tokens = [e.get("prompt_tokens_est", 0) for e in entries]
1151+
outputs = [e.get("output_size", 0) for e in entries]
1152+
1153+
avg_dur = sum(durations) / total if total else 0
1154+
avg_tok = sum(tokens) / total if total else 0
1155+
avg_out = sum(outputs) / total if total else 0
1156+
1157+
# Time range
1158+
first_ts = entries[0].get("timestamp", "?")
1159+
last_ts = entries[-1].get("timestamp", "?")
1160+
1161+
# Provider/model breakdown
1162+
providers = {}
1163+
for e in entries:
1164+
p = e.get("provider", "?")
1165+
providers[p] = providers.get(p, 0) + 1
1166+
1167+
models = {}
1168+
for e in entries:
1169+
m = e.get("model", "?")
1170+
models[m] = models.get(m, 0) + 1
1171+
1172+
print(f"alive — metrics summary ({total} sessions)")
1173+
print(f" Period: {first_ts[:10]} to {last_ts[:10]}")
1174+
print(f" Success rate: {successes}/{total} ({100*successes/total:.0f}%)")
1175+
print(f" Avg duration: {avg_dur:.1f}s")
1176+
print(f" Avg tokens: {avg_tok:,.0f}")
1177+
print(f" Avg output: {avg_out:,.0f} chars")
1178+
print(f" Total time: {sum(durations)/3600:.1f}h")
1179+
print()
1180+
1181+
if len(providers) > 1 or len(models) > 1:
1182+
print(" Providers:")
1183+
for p, count in sorted(providers.items(), key=lambda x: -x[1]):
1184+
print(f" {p}: {count} sessions")
1185+
print(" Models:")
1186+
for m, count in sorted(models.items(), key=lambda x: -x[1]):
1187+
print(f" {m}: {count} sessions")
1188+
print()
1189+
1190+
# Last 5 sessions
1191+
print(" Recent sessions:")
1192+
for e in entries[-5:]:
1193+
ts = e.get("timestamp", "?")[:19]
1194+
dur = e.get("duration_seconds", 0)
1195+
ok = "OK" if e.get("success") else "FAIL"
1196+
tok = e.get("prompt_tokens_est", 0)
1197+
print(f" {ts} {dur:>6.1f}s {tok:>6,} tok {ok}")
1198+
1199+
9891200
def check_config():
9901201
"""Validate configuration and show what would be loaded. No LLM call."""
9911202
load_env()
@@ -1243,8 +1454,36 @@ def main():
12431454
"--dashboard-only", action="store_true",
12441455
help="Run only the dashboard, no wake loop",
12451456
)
1457+
parser.add_argument(
1458+
"--scaffold-adapter", metavar="NAME",
1459+
help="Create a new adapter template in comms/",
1460+
)
1461+
parser.add_argument(
1462+
"--test-adapters", action="store_true",
1463+
help="Dry-run all adapters and validate their output",
1464+
)
1465+
parser.add_argument(
1466+
"--metrics", action="store_true",
1467+
help="Show session metrics summary",
1468+
)
1469+
parser.add_argument(
1470+
"--soul", metavar="PATH",
1471+
help="Use a custom soul file instead of soul.md",
1472+
)
12461473
args = parser.parse_args()
12471474

1475+
if args.scaffold_adapter:
1476+
scaffold_adapter(args.scaffold_adapter)
1477+
return
1478+
1479+
if args.test_adapters:
1480+
test_adapters()
1481+
return
1482+
1483+
if args.metrics:
1484+
show_metrics()
1485+
return
1486+
12481487
if args.check:
12491488
check_config()
12501489
return
@@ -1253,6 +1492,11 @@ def main():
12531492
run_demo()
12541493
return
12551494

1495+
# Custom soul file
1496+
if args.soul:
1497+
global SOUL_FILE
1498+
SOUL_FILE = Path(args.soul).resolve()
1499+
12561500
load_env()
12571501

12581502
# Dashboard mode

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
44

55
[project]
66
name = "alive-framework"
7-
version = "1.0.0"
7+
version = "1.1.0"
88
description = "Everything you need to make an AI autonomous. In one file."
99
readme = "README.md"
1010
license = "MIT"

0 commit comments

Comments
 (0)