squads-cli 0.4.11 → 0.4.13
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.
- package/README.md +24 -4
- package/dist/chunk-3TSY2K7R.js +473 -0
- package/dist/chunk-3TSY2K7R.js.map +1 -0
- package/dist/cli.js +4292 -1091
- package/dist/cli.js.map +1 -1
- package/dist/index.d.ts +54 -0
- package/dist/index.js +5 -1
- package/dist/index.js.map +1 -1
- package/dist/squad-parser-YRE2FEAA.js +31 -0
- package/dist/squad-parser-YRE2FEAA.js.map +1 -0
- package/docker/docker-compose.engram.yml +26 -0
- package/docker/docker-compose.yml +118 -78
- package/docker/squads-bridge/squads_bridge.py +534 -8
- package/package.json +12 -8
|
@@ -7,6 +7,7 @@ Forwards conversations to engram/mem0 for embeddings and graph storage.
|
|
|
7
7
|
import os
|
|
8
8
|
import json
|
|
9
9
|
import gzip
|
|
10
|
+
import hashlib
|
|
10
11
|
import threading
|
|
11
12
|
import time
|
|
12
13
|
import requests
|
|
@@ -22,7 +23,8 @@ app = Flask(__name__)
|
|
|
22
23
|
# Configuration
|
|
23
24
|
DEBUG_MODE = os.environ.get("DEBUG", "1") == "1"
|
|
24
25
|
LANGFUSE_ENABLED = os.environ.get("LANGFUSE_ENABLED", "false").lower() == "true"
|
|
25
|
-
|
|
26
|
+
# Monthly quota based on Anthropic plan (Max5 = $200/month)
|
|
27
|
+
MONTHLY_QUOTA = float(os.environ.get("SQUADS_MONTHLY_QUOTA", "200.0"))
|
|
26
28
|
recent_logs = deque(maxlen=50)
|
|
27
29
|
|
|
28
30
|
# Engram/mem0 configuration for memory extraction
|
|
@@ -128,8 +130,8 @@ def get_realtime_stats() -> dict:
|
|
|
128
130
|
"output_tokens": output_tokens,
|
|
129
131
|
"generations": generations,
|
|
130
132
|
"by_squad": by_squad,
|
|
131
|
-
"budget_remaining":
|
|
132
|
-
"budget_pct": (cost /
|
|
133
|
+
"budget_remaining": MONTHLY_QUOTA - cost,
|
|
134
|
+
"budget_pct": (cost / MONTHLY_QUOTA) * 100 if MONTHLY_QUOTA > 0 else 0,
|
|
133
135
|
}
|
|
134
136
|
except Exception as e:
|
|
135
137
|
print(f"Redis stats error: {e}")
|
|
@@ -763,7 +765,7 @@ def stats():
|
|
|
763
765
|
"cost_usd": realtime["cost_usd"],
|
|
764
766
|
},
|
|
765
767
|
"budget": {
|
|
766
|
-
"daily":
|
|
768
|
+
"daily": MONTHLY_QUOTA,
|
|
767
769
|
"used": realtime["cost_usd"],
|
|
768
770
|
"remaining": realtime["budget_remaining"],
|
|
769
771
|
"used_pct": realtime["budget_pct"],
|
|
@@ -820,10 +822,10 @@ def stats():
|
|
|
820
822
|
"cost_usd": cost_usd,
|
|
821
823
|
},
|
|
822
824
|
"budget": {
|
|
823
|
-
"daily":
|
|
825
|
+
"daily": MONTHLY_QUOTA,
|
|
824
826
|
"used": cost_usd,
|
|
825
|
-
"remaining":
|
|
826
|
-
"used_pct": (cost_usd /
|
|
827
|
+
"remaining": MONTHLY_QUOTA - cost_usd,
|
|
828
|
+
"used_pct": (cost_usd / MONTHLY_QUOTA) * 100 if MONTHLY_QUOTA > 0 else 0,
|
|
827
829
|
},
|
|
828
830
|
"by_squad": [dict(r) for r in by_squad],
|
|
829
831
|
"langfuse_enabled": langfuse is not None,
|
|
@@ -1757,6 +1759,530 @@ def brief_history():
|
|
|
1757
1759
|
return jsonify({"error": str(e)}), 500
|
|
1758
1760
|
|
|
1759
1761
|
|
|
1762
|
+
# =============================================================================
|
|
1763
|
+
# Execution Gates API - Pre-execution safety checks
|
|
1764
|
+
# =============================================================================
|
|
1765
|
+
|
|
1766
|
+
@app.route("/api/execution/preflight", methods=["POST"])
|
|
1767
|
+
def preflight_check():
|
|
1768
|
+
"""Check all execution gates before agent runs.
|
|
1769
|
+
|
|
1770
|
+
Returns:
|
|
1771
|
+
{
|
|
1772
|
+
"allowed": bool,
|
|
1773
|
+
"gates": {
|
|
1774
|
+
"budget": {"ok": bool, "used": float, "limit": float, "remaining": float},
|
|
1775
|
+
"cooldown": {"ok": bool, "elapsed_sec": int, "min_gap_sec": int}
|
|
1776
|
+
}
|
|
1777
|
+
}
|
|
1778
|
+
"""
|
|
1779
|
+
try:
|
|
1780
|
+
data = request.get_json() or {}
|
|
1781
|
+
squad = data.get("squad", "hq")
|
|
1782
|
+
agent = data.get("agent")
|
|
1783
|
+
min_cooldown = int(data.get("min_cooldown_sec", 300)) # 5 min default
|
|
1784
|
+
|
|
1785
|
+
# Quota gate - check monthly spend against plan quota
|
|
1786
|
+
# Query monthly usage from Postgres (Redis only has daily stats)
|
|
1787
|
+
conn = get_db()
|
|
1788
|
+
with conn.cursor(cursor_factory=RealDictCursor) as cur:
|
|
1789
|
+
cur.execute("""
|
|
1790
|
+
SELECT COALESCE(SUM(cost_usd), 0) as cost_usd
|
|
1791
|
+
FROM squads.llm_generations
|
|
1792
|
+
WHERE created_at >= DATE_TRUNC('month', CURRENT_DATE)
|
|
1793
|
+
""")
|
|
1794
|
+
result = cur.fetchone()
|
|
1795
|
+
monthly_used = float(result["cost_usd"]) if result else 0
|
|
1796
|
+
conn.close()
|
|
1797
|
+
|
|
1798
|
+
quota_gate = {
|
|
1799
|
+
"ok": monthly_used < MONTHLY_QUOTA,
|
|
1800
|
+
"used": round(monthly_used, 2),
|
|
1801
|
+
"limit": MONTHLY_QUOTA,
|
|
1802
|
+
"remaining": round(MONTHLY_QUOTA - monthly_used, 2),
|
|
1803
|
+
"period": "month"
|
|
1804
|
+
}
|
|
1805
|
+
|
|
1806
|
+
# Cooldown gate (from existing tasks table)
|
|
1807
|
+
cooldown_gate = {"ok": True, "elapsed_sec": None, "min_gap_sec": min_cooldown}
|
|
1808
|
+
|
|
1809
|
+
if agent:
|
|
1810
|
+
conn = get_db()
|
|
1811
|
+
with conn.cursor(cursor_factory=RealDictCursor) as cur:
|
|
1812
|
+
cur.execute("""
|
|
1813
|
+
SELECT MAX(started_at) as last_run
|
|
1814
|
+
FROM squads.tasks
|
|
1815
|
+
WHERE squad = %s AND agent = %s
|
|
1816
|
+
""", (squad, agent))
|
|
1817
|
+
result = cur.fetchone()
|
|
1818
|
+
conn.close()
|
|
1819
|
+
|
|
1820
|
+
if result and result["last_run"]:
|
|
1821
|
+
last_run = result["last_run"]
|
|
1822
|
+
# Handle timezone-aware datetime
|
|
1823
|
+
now = datetime.now(last_run.tzinfo) if last_run.tzinfo else datetime.now()
|
|
1824
|
+
elapsed = (now - last_run).total_seconds()
|
|
1825
|
+
cooldown_gate = {
|
|
1826
|
+
"ok": elapsed >= min_cooldown,
|
|
1827
|
+
"elapsed_sec": int(elapsed),
|
|
1828
|
+
"min_gap_sec": min_cooldown,
|
|
1829
|
+
"last_run": last_run.isoformat()
|
|
1830
|
+
}
|
|
1831
|
+
|
|
1832
|
+
# Quota gate is informational only - never blocks execution
|
|
1833
|
+
# Track as KPI, not as enforcement (real limits come from Anthropic subscription)
|
|
1834
|
+
allowed = cooldown_gate["ok"]
|
|
1835
|
+
|
|
1836
|
+
if DEBUG_MODE:
|
|
1837
|
+
print(f"[PREFLIGHT] {squad}/{agent}: quota=${quota_gate['used']}/mo (KPI) cooldown={'OK' if cooldown_gate['ok'] else 'BLOCKED'}")
|
|
1838
|
+
|
|
1839
|
+
return jsonify({
|
|
1840
|
+
"allowed": allowed,
|
|
1841
|
+
"squad": squad,
|
|
1842
|
+
"agent": agent,
|
|
1843
|
+
"gates": {
|
|
1844
|
+
"quota": quota_gate,
|
|
1845
|
+
"cooldown": cooldown_gate
|
|
1846
|
+
}
|
|
1847
|
+
}), 200
|
|
1848
|
+
|
|
1849
|
+
except Exception as e:
|
|
1850
|
+
import traceback
|
|
1851
|
+
traceback.print_exc()
|
|
1852
|
+
# Fail open - if check fails, allow execution
|
|
1853
|
+
return jsonify({
|
|
1854
|
+
"allowed": True,
|
|
1855
|
+
"error": str(e),
|
|
1856
|
+
"gates": {}
|
|
1857
|
+
}), 200
|
|
1858
|
+
|
|
1859
|
+
|
|
1860
|
+
@app.route("/api/learnings/relevant", methods=["GET"])
|
|
1861
|
+
def get_relevant_learnings():
|
|
1862
|
+
"""Get recent learnings for prompt injection.
|
|
1863
|
+
|
|
1864
|
+
Queries conversations marked as 'learning' type, ordered by importance.
|
|
1865
|
+
|
|
1866
|
+
Query params:
|
|
1867
|
+
squad: Filter by squad (optional)
|
|
1868
|
+
limit: Max results (default 5)
|
|
1869
|
+
|
|
1870
|
+
Returns:
|
|
1871
|
+
{
|
|
1872
|
+
"squad": str,
|
|
1873
|
+
"count": int,
|
|
1874
|
+
"learnings": [{"content": str, "importance": str, "created_at": str}]
|
|
1875
|
+
}
|
|
1876
|
+
"""
|
|
1877
|
+
try:
|
|
1878
|
+
squad = request.args.get("squad")
|
|
1879
|
+
limit = min(int(request.args.get("limit", 5)), 20)
|
|
1880
|
+
|
|
1881
|
+
conn = get_db()
|
|
1882
|
+
with conn.cursor(cursor_factory=RealDictCursor) as cur:
|
|
1883
|
+
# Query conversations marked as learnings
|
|
1884
|
+
# Order by importance (high > normal > low), then recency
|
|
1885
|
+
if squad:
|
|
1886
|
+
cur.execute("""
|
|
1887
|
+
SELECT content, importance, created_at, squad
|
|
1888
|
+
FROM squads.conversations
|
|
1889
|
+
WHERE message_type = 'learning'
|
|
1890
|
+
AND (squad = %s OR squad IS NULL)
|
|
1891
|
+
ORDER BY
|
|
1892
|
+
CASE importance WHEN 'high' THEN 1 WHEN 'normal' THEN 2 ELSE 3 END,
|
|
1893
|
+
created_at DESC
|
|
1894
|
+
LIMIT %s
|
|
1895
|
+
""", (squad, limit))
|
|
1896
|
+
else:
|
|
1897
|
+
cur.execute("""
|
|
1898
|
+
SELECT content, importance, created_at, squad
|
|
1899
|
+
FROM squads.conversations
|
|
1900
|
+
WHERE message_type = 'learning'
|
|
1901
|
+
ORDER BY
|
|
1902
|
+
CASE importance WHEN 'high' THEN 1 WHEN 'normal' THEN 2 ELSE 3 END,
|
|
1903
|
+
created_at DESC
|
|
1904
|
+
LIMIT %s
|
|
1905
|
+
""", (limit,))
|
|
1906
|
+
|
|
1907
|
+
learnings = cur.fetchall()
|
|
1908
|
+
conn.close()
|
|
1909
|
+
|
|
1910
|
+
if DEBUG_MODE:
|
|
1911
|
+
print(f"[LEARNINGS] Found {len(learnings)} learnings for squad={squad}")
|
|
1912
|
+
|
|
1913
|
+
return jsonify({
|
|
1914
|
+
"squad": squad,
|
|
1915
|
+
"count": len(learnings),
|
|
1916
|
+
"learnings": [{
|
|
1917
|
+
"content": l["content"][:500] if l["content"] else "",
|
|
1918
|
+
"importance": l["importance"] or "normal",
|
|
1919
|
+
"squad": l["squad"],
|
|
1920
|
+
"created_at": l["created_at"].isoformat() if l["created_at"] else None
|
|
1921
|
+
} for l in learnings]
|
|
1922
|
+
}), 200
|
|
1923
|
+
|
|
1924
|
+
except Exception as e:
|
|
1925
|
+
import traceback
|
|
1926
|
+
traceback.print_exc()
|
|
1927
|
+
return jsonify({
|
|
1928
|
+
"squad": request.args.get("squad"),
|
|
1929
|
+
"count": 0,
|
|
1930
|
+
"learnings": [],
|
|
1931
|
+
"error": str(e)
|
|
1932
|
+
}), 200
|
|
1933
|
+
|
|
1934
|
+
|
|
1935
|
+
# =============================================================================
|
|
1936
|
+
# Dimension Sync API - Sync squad/agent definitions from CLI
|
|
1937
|
+
# =============================================================================
|
|
1938
|
+
|
|
1939
|
+
@app.route("/api/sync/dimensions", methods=["POST"])
|
|
1940
|
+
def sync_dimensions():
|
|
1941
|
+
"""Sync squad and agent definitions to dimension tables.
|
|
1942
|
+
|
|
1943
|
+
Expects:
|
|
1944
|
+
{
|
|
1945
|
+
"squads": [{"name": str, "mission": str, "domain": str, ...}],
|
|
1946
|
+
"agents": [{"name": str, "squad": str, "role": str, ...}]
|
|
1947
|
+
}
|
|
1948
|
+
|
|
1949
|
+
Returns:
|
|
1950
|
+
{"synced_squads": int, "synced_agents": int}
|
|
1951
|
+
"""
|
|
1952
|
+
try:
|
|
1953
|
+
data = request.get_json() or {}
|
|
1954
|
+
squads = data.get("squads", [])
|
|
1955
|
+
agents = data.get("agents", [])
|
|
1956
|
+
|
|
1957
|
+
conn = get_db()
|
|
1958
|
+
synced_squads = 0
|
|
1959
|
+
synced_agents = 0
|
|
1960
|
+
|
|
1961
|
+
with conn.cursor() as cur:
|
|
1962
|
+
# Upsert squads
|
|
1963
|
+
for squad in squads:
|
|
1964
|
+
cur.execute("""
|
|
1965
|
+
INSERT INTO squads.dim_squads (
|
|
1966
|
+
squad_name, mission, domain, default_provider,
|
|
1967
|
+
daily_budget, cooldown_seconds, metadata, updated_at
|
|
1968
|
+
) VALUES (%s, %s, %s, %s, %s, %s, %s, NOW())
|
|
1969
|
+
ON CONFLICT (squad_name) DO UPDATE SET
|
|
1970
|
+
mission = EXCLUDED.mission,
|
|
1971
|
+
domain = EXCLUDED.domain,
|
|
1972
|
+
default_provider = EXCLUDED.default_provider,
|
|
1973
|
+
daily_budget = EXCLUDED.daily_budget,
|
|
1974
|
+
cooldown_seconds = EXCLUDED.cooldown_seconds,
|
|
1975
|
+
metadata = EXCLUDED.metadata,
|
|
1976
|
+
updated_at = NOW()
|
|
1977
|
+
""", (
|
|
1978
|
+
squad["name"],
|
|
1979
|
+
squad.get("mission"),
|
|
1980
|
+
squad.get("domain"),
|
|
1981
|
+
squad.get("default_provider", "anthropic"),
|
|
1982
|
+
squad.get("daily_budget", 50),
|
|
1983
|
+
squad.get("cooldown_seconds", 300),
|
|
1984
|
+
json.dumps(squad.get("metadata", {})),
|
|
1985
|
+
))
|
|
1986
|
+
synced_squads += 1
|
|
1987
|
+
|
|
1988
|
+
# Upsert agents (need squad_id)
|
|
1989
|
+
for agent in agents:
|
|
1990
|
+
# Get squad_id
|
|
1991
|
+
cur.execute(
|
|
1992
|
+
"SELECT id FROM squads.dim_squads WHERE squad_name = %s",
|
|
1993
|
+
(agent["squad"],)
|
|
1994
|
+
)
|
|
1995
|
+
row = cur.fetchone()
|
|
1996
|
+
squad_id = row[0] if row else None
|
|
1997
|
+
|
|
1998
|
+
if squad_id:
|
|
1999
|
+
cur.execute("""
|
|
2000
|
+
INSERT INTO squads.dim_agents (
|
|
2001
|
+
agent_name, squad_id, role, purpose, provider,
|
|
2002
|
+
trigger_type, mcp_servers, skills, metadata, updated_at
|
|
2003
|
+
) VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, NOW())
|
|
2004
|
+
ON CONFLICT (agent_name, squad_id) DO UPDATE SET
|
|
2005
|
+
role = EXCLUDED.role,
|
|
2006
|
+
purpose = EXCLUDED.purpose,
|
|
2007
|
+
provider = EXCLUDED.provider,
|
|
2008
|
+
trigger_type = EXCLUDED.trigger_type,
|
|
2009
|
+
mcp_servers = EXCLUDED.mcp_servers,
|
|
2010
|
+
skills = EXCLUDED.skills,
|
|
2011
|
+
metadata = EXCLUDED.metadata,
|
|
2012
|
+
updated_at = NOW()
|
|
2013
|
+
""", (
|
|
2014
|
+
agent["name"],
|
|
2015
|
+
squad_id,
|
|
2016
|
+
agent.get("role"),
|
|
2017
|
+
agent.get("purpose"),
|
|
2018
|
+
agent.get("provider"),
|
|
2019
|
+
agent.get("trigger_type", "manual"),
|
|
2020
|
+
agent.get("mcp_servers", []),
|
|
2021
|
+
agent.get("skills", []),
|
|
2022
|
+
json.dumps(agent.get("metadata", {})),
|
|
2023
|
+
))
|
|
2024
|
+
synced_agents += 1
|
|
2025
|
+
|
|
2026
|
+
conn.commit()
|
|
2027
|
+
|
|
2028
|
+
conn.close()
|
|
2029
|
+
|
|
2030
|
+
if DEBUG_MODE:
|
|
2031
|
+
print(f"[SYNC] Synced {synced_squads} squads, {synced_agents} agents")
|
|
2032
|
+
|
|
2033
|
+
return jsonify({
|
|
2034
|
+
"synced_squads": synced_squads,
|
|
2035
|
+
"synced_agents": synced_agents
|
|
2036
|
+
}), 200
|
|
2037
|
+
|
|
2038
|
+
except Exception as e:
|
|
2039
|
+
import traceback
|
|
2040
|
+
traceback.print_exc()
|
|
2041
|
+
return jsonify({"error": str(e)}), 500
|
|
2042
|
+
|
|
2043
|
+
|
|
2044
|
+
@app.route("/api/sync/learnings", methods=["POST"])
|
|
2045
|
+
def sync_learnings():
|
|
2046
|
+
"""Sync learnings from CLI to Postgres conversations table.
|
|
2047
|
+
|
|
2048
|
+
Body:
|
|
2049
|
+
learnings: list of {squad, agent, content, category, importance, source_file}
|
|
2050
|
+
|
|
2051
|
+
Returns:
|
|
2052
|
+
{imported: int, skipped: int}
|
|
2053
|
+
"""
|
|
2054
|
+
try:
|
|
2055
|
+
data = request.get_json() or {}
|
|
2056
|
+
learnings = data.get("learnings", [])
|
|
2057
|
+
|
|
2058
|
+
if not learnings:
|
|
2059
|
+
return jsonify({"imported": 0, "skipped": 0}), 200
|
|
2060
|
+
|
|
2061
|
+
conn = get_db()
|
|
2062
|
+
imported = 0
|
|
2063
|
+
skipped = 0
|
|
2064
|
+
|
|
2065
|
+
with conn.cursor(cursor_factory=RealDictCursor) as cur:
|
|
2066
|
+
for learning in learnings:
|
|
2067
|
+
# Check for duplicate (same content hash)
|
|
2068
|
+
content = learning.get("content", "")
|
|
2069
|
+
content_hash = hashlib.md5(content.encode()).hexdigest()
|
|
2070
|
+
|
|
2071
|
+
cur.execute("""
|
|
2072
|
+
SELECT id FROM squads.conversations
|
|
2073
|
+
WHERE message_type = 'learning'
|
|
2074
|
+
AND metadata->>'content_hash' = %s
|
|
2075
|
+
""", (content_hash,))
|
|
2076
|
+
|
|
2077
|
+
if cur.fetchone():
|
|
2078
|
+
skipped += 1
|
|
2079
|
+
continue
|
|
2080
|
+
|
|
2081
|
+
# Insert into conversations table as learning
|
|
2082
|
+
cur.execute("""
|
|
2083
|
+
INSERT INTO squads.conversations (
|
|
2084
|
+
role, content, message_type, importance, squad, agent, metadata
|
|
2085
|
+
) VALUES (
|
|
2086
|
+
'system', %s, 'learning', %s, %s, %s, %s
|
|
2087
|
+
)
|
|
2088
|
+
""", (
|
|
2089
|
+
content,
|
|
2090
|
+
learning.get("importance", "normal"),
|
|
2091
|
+
learning.get("squad"),
|
|
2092
|
+
learning.get("agent"),
|
|
2093
|
+
json.dumps({
|
|
2094
|
+
"category": learning.get("category", "insight"),
|
|
2095
|
+
"source_file": learning.get("source_file"),
|
|
2096
|
+
"content_hash": content_hash,
|
|
2097
|
+
"imported_at": datetime.now().isoformat(),
|
|
2098
|
+
}),
|
|
2099
|
+
))
|
|
2100
|
+
imported += 1
|
|
2101
|
+
|
|
2102
|
+
conn.commit()
|
|
2103
|
+
|
|
2104
|
+
conn.close()
|
|
2105
|
+
|
|
2106
|
+
if DEBUG_MODE:
|
|
2107
|
+
print(f"[SYNC] Imported {imported} learnings, skipped {skipped} duplicates")
|
|
2108
|
+
|
|
2109
|
+
return jsonify({"imported": imported, "skipped": skipped}), 200
|
|
2110
|
+
|
|
2111
|
+
except Exception as e:
|
|
2112
|
+
import traceback
|
|
2113
|
+
traceback.print_exc()
|
|
2114
|
+
return jsonify({"error": str(e)}), 500
|
|
2115
|
+
|
|
2116
|
+
|
|
2117
|
+
# =============================================================================
|
|
2118
|
+
# Autonomy Score API - Calculate and return autonomy metrics
|
|
2119
|
+
# =============================================================================
|
|
2120
|
+
|
|
2121
|
+
@app.route("/api/autonomy/score", methods=["GET"])
|
|
2122
|
+
def get_autonomy_score():
|
|
2123
|
+
"""Calculate current autonomy score.
|
|
2124
|
+
|
|
2125
|
+
Query params:
|
|
2126
|
+
squad: Filter by squad (optional)
|
|
2127
|
+
period: Time period - today, week, month (default: today)
|
|
2128
|
+
|
|
2129
|
+
Returns:
|
|
2130
|
+
{
|
|
2131
|
+
"overall_score": int (0-100),
|
|
2132
|
+
"confidence_level": str (low/medium/high),
|
|
2133
|
+
"components": {
|
|
2134
|
+
"budget_compliance": int,
|
|
2135
|
+
"cooldown_compliance": int,
|
|
2136
|
+
"quality_score": int,
|
|
2137
|
+
"success_rate": int,
|
|
2138
|
+
"learning_utilization": int
|
|
2139
|
+
},
|
|
2140
|
+
"execution_stats": {...}
|
|
2141
|
+
}
|
|
2142
|
+
"""
|
|
2143
|
+
try:
|
|
2144
|
+
squad = request.args.get("squad")
|
|
2145
|
+
period = request.args.get("period", "today")
|
|
2146
|
+
|
|
2147
|
+
# Determine date range (tasks uses started_at, others use created_at)
|
|
2148
|
+
if period == "today":
|
|
2149
|
+
tasks_date_filter = "started_at >= CURRENT_DATE"
|
|
2150
|
+
date_filter = "created_at >= CURRENT_DATE"
|
|
2151
|
+
elif period == "week":
|
|
2152
|
+
tasks_date_filter = "started_at >= CURRENT_DATE - INTERVAL '7 days'"
|
|
2153
|
+
date_filter = "created_at >= CURRENT_DATE - INTERVAL '7 days'"
|
|
2154
|
+
elif period == "month":
|
|
2155
|
+
tasks_date_filter = "started_at >= CURRENT_DATE - INTERVAL '30 days'"
|
|
2156
|
+
date_filter = "created_at >= CURRENT_DATE - INTERVAL '30 days'"
|
|
2157
|
+
else:
|
|
2158
|
+
tasks_date_filter = "started_at >= CURRENT_DATE"
|
|
2159
|
+
date_filter = "created_at >= CURRENT_DATE"
|
|
2160
|
+
|
|
2161
|
+
conn = get_db()
|
|
2162
|
+
components = {}
|
|
2163
|
+
|
|
2164
|
+
with conn.cursor(cursor_factory=RealDictCursor) as cur:
|
|
2165
|
+
# Squad filter
|
|
2166
|
+
squad_filter = f"AND squad = '{squad}'" if squad else ""
|
|
2167
|
+
|
|
2168
|
+
# 1. Usage KPI (monthly spend tracking - informational, not a gate)
|
|
2169
|
+
cur.execute("""
|
|
2170
|
+
SELECT COALESCE(SUM(cost_usd), 0) as cost_usd
|
|
2171
|
+
FROM squads.llm_generations
|
|
2172
|
+
WHERE created_at >= DATE_TRUNC('month', CURRENT_DATE)
|
|
2173
|
+
""")
|
|
2174
|
+
monthly_result = cur.fetchone()
|
|
2175
|
+
monthly_used = float(monthly_result["cost_usd"]) if monthly_result else 0
|
|
2176
|
+
# KPI: Track usage but don't penalize - real limits come from Anthropic subscription
|
|
2177
|
+
# Score represents "how actively we're using our capacity" (higher = more active)
|
|
2178
|
+
components["usage_kpi"] = min(100, int(monthly_used / 10)) # $1000/mo = 100% utilization
|
|
2179
|
+
|
|
2180
|
+
# 2. Success rate from tasks
|
|
2181
|
+
cur.execute(f"""
|
|
2182
|
+
SELECT
|
|
2183
|
+
COUNT(*) as total,
|
|
2184
|
+
COUNT(*) FILTER (WHERE status = 'completed' AND success = true) as successful
|
|
2185
|
+
FROM squads.tasks
|
|
2186
|
+
WHERE {tasks_date_filter} {squad_filter}
|
|
2187
|
+
""")
|
|
2188
|
+
task_stats = cur.fetchone()
|
|
2189
|
+
total_tasks = task_stats["total"] or 0
|
|
2190
|
+
successful_tasks = task_stats["successful"] or 0
|
|
2191
|
+
components["success_rate"] = int(100 * successful_tasks / total_tasks) if total_tasks > 0 else 100
|
|
2192
|
+
|
|
2193
|
+
# 3. Quality score from feedback
|
|
2194
|
+
cur.execute(f"""
|
|
2195
|
+
SELECT AVG(quality_score) as avg_score, COUNT(*) as count
|
|
2196
|
+
FROM squads.task_feedback
|
|
2197
|
+
WHERE {date_filter}
|
|
2198
|
+
""")
|
|
2199
|
+
feedback_stats = cur.fetchone()
|
|
2200
|
+
avg_quality = feedback_stats["avg_score"] or 3.5 # Default to neutral
|
|
2201
|
+
components["quality_score"] = int(avg_quality * 20) # Scale 1-5 to 0-100
|
|
2202
|
+
|
|
2203
|
+
# 4. Cooldown compliance (check for rapid re-executions)
|
|
2204
|
+
cur.execute(f"""
|
|
2205
|
+
WITH exec_gaps AS (
|
|
2206
|
+
SELECT
|
|
2207
|
+
squad, agent,
|
|
2208
|
+
started_at,
|
|
2209
|
+
LAG(started_at) OVER (PARTITION BY squad, agent ORDER BY started_at) as prev_start
|
|
2210
|
+
FROM squads.tasks
|
|
2211
|
+
WHERE {tasks_date_filter} {squad_filter}
|
|
2212
|
+
)
|
|
2213
|
+
SELECT
|
|
2214
|
+
COUNT(*) as total,
|
|
2215
|
+
COUNT(*) FILTER (WHERE EXTRACT(EPOCH FROM (started_at - prev_start)) >= 300 OR prev_start IS NULL) as compliant
|
|
2216
|
+
FROM exec_gaps
|
|
2217
|
+
""")
|
|
2218
|
+
cooldown_stats = cur.fetchone()
|
|
2219
|
+
total_execs = cooldown_stats["total"] or 0
|
|
2220
|
+
compliant_execs = cooldown_stats["compliant"] or 0
|
|
2221
|
+
components["cooldown_compliance"] = int(100 * compliant_execs / total_execs) if total_execs > 0 else 100
|
|
2222
|
+
|
|
2223
|
+
# 5. Learning utilization (check if learnings exist in conversations)
|
|
2224
|
+
cur.execute("""
|
|
2225
|
+
SELECT COUNT(*) as learning_count
|
|
2226
|
+
FROM squads.conversations
|
|
2227
|
+
WHERE message_type = 'learning'
|
|
2228
|
+
AND created_at >= CURRENT_DATE - INTERVAL '30 days'
|
|
2229
|
+
""")
|
|
2230
|
+
learning_stats = cur.fetchone()
|
|
2231
|
+
learning_count = learning_stats["learning_count"] or 0
|
|
2232
|
+
# Score based on having learnings available (simple heuristic)
|
|
2233
|
+
components["learning_utilization"] = min(100, learning_count * 10) if learning_count > 0 else 0
|
|
2234
|
+
|
|
2235
|
+
conn.close()
|
|
2236
|
+
|
|
2237
|
+
# Calculate weighted overall score
|
|
2238
|
+
# Note: usage_kpi excluded from score calculation (it's informational only)
|
|
2239
|
+
weights = {
|
|
2240
|
+
"success_rate": 0.35,
|
|
2241
|
+
"quality_score": 0.30,
|
|
2242
|
+
"cooldown_compliance": 0.20,
|
|
2243
|
+
"learning_utilization": 0.15
|
|
2244
|
+
}
|
|
2245
|
+
|
|
2246
|
+
overall_score = int(sum(
|
|
2247
|
+
components.get(k, 0) * v for k, v in weights.items()
|
|
2248
|
+
))
|
|
2249
|
+
|
|
2250
|
+
# Determine confidence level
|
|
2251
|
+
if overall_score >= 75:
|
|
2252
|
+
confidence_level = "high"
|
|
2253
|
+
elif overall_score >= 50:
|
|
2254
|
+
confidence_level = "medium"
|
|
2255
|
+
else:
|
|
2256
|
+
confidence_level = "low"
|
|
2257
|
+
|
|
2258
|
+
return jsonify({
|
|
2259
|
+
"overall_score": overall_score,
|
|
2260
|
+
"confidence_level": confidence_level,
|
|
2261
|
+
"period": period,
|
|
2262
|
+
"squad": squad,
|
|
2263
|
+
"components": components,
|
|
2264
|
+
"execution_stats": {
|
|
2265
|
+
"total_tasks": total_tasks,
|
|
2266
|
+
"successful_tasks": successful_tasks,
|
|
2267
|
+
"monthly_spend_usd": round(monthly_used, 2),
|
|
2268
|
+
"learning_count": learning_count
|
|
2269
|
+
},
|
|
2270
|
+
"usage_kpi": {
|
|
2271
|
+
"monthly_spend": round(monthly_used, 2),
|
|
2272
|
+
"note": "Track via /usage for real Anthropic limits"
|
|
2273
|
+
}
|
|
2274
|
+
}), 200
|
|
2275
|
+
|
|
2276
|
+
except Exception as e:
|
|
2277
|
+
import traceback
|
|
2278
|
+
traceback.print_exc()
|
|
2279
|
+
return jsonify({
|
|
2280
|
+
"overall_score": 50,
|
|
2281
|
+
"confidence_level": "unknown",
|
|
2282
|
+
"error": str(e)
|
|
2283
|
+
}), 200
|
|
2284
|
+
|
|
2285
|
+
|
|
1760
2286
|
if __name__ == "__main__":
|
|
1761
2287
|
port = int(os.environ.get("PORT", 8080))
|
|
1762
2288
|
print(f"Starting Squads Bridge on port {port}")
|
|
@@ -1764,7 +2290,7 @@ if __name__ == "__main__":
|
|
|
1764
2290
|
print(f" Redis: {'connected' if redis_client else 'disabled'}")
|
|
1765
2291
|
print(f" Langfuse: {'enabled' if LANGFUSE_ENABLED else 'disabled'}")
|
|
1766
2292
|
print(f" Engram: {'enabled -> ' + ENGRAM_URL if ENGRAM_ENABLED else 'disabled'}")
|
|
1767
|
-
print(f"
|
|
2293
|
+
print(f" Usage KPI: tracking (real limits via /usage)")
|
|
1768
2294
|
|
|
1769
2295
|
# Start background conversation processor
|
|
1770
2296
|
if redis_client:
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "squads-cli",
|
|
3
|
-
"version": "0.4.
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "0.4.13",
|
|
4
|
+
"description": "Open source CLI for AI agent coordination and execution. Domain-aligned squads with persistent memory and goals.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
7
7
|
"squads": "./dist/cli.js"
|
|
@@ -39,13 +39,17 @@
|
|
|
39
39
|
"prepublishOnly": "npm run build"
|
|
40
40
|
},
|
|
41
41
|
"keywords": [
|
|
42
|
-
"ai",
|
|
43
|
-
"agents",
|
|
44
|
-
"squads",
|
|
45
|
-
"cli",
|
|
46
|
-
"automation",
|
|
47
42
|
"claude",
|
|
48
|
-
"
|
|
43
|
+
"claude-code",
|
|
44
|
+
"anthropic",
|
|
45
|
+
"mcp",
|
|
46
|
+
"agent-sdk",
|
|
47
|
+
"multi-agent",
|
|
48
|
+
"orchestration",
|
|
49
|
+
"ai-agents",
|
|
50
|
+
"persistent-memory",
|
|
51
|
+
"squads",
|
|
52
|
+
"cli"
|
|
49
53
|
],
|
|
50
54
|
"author": "Agents Squads <hello@agents-squads.com>",
|
|
51
55
|
"license": "MIT",
|