arkaos 3.58.0 → 3.60.0
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/VERSION
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
3.
|
|
1
|
+
3.60.0
|
|
@@ -37,6 +37,25 @@ const { data, status, error, refresh } = await fetchApi<DeptDetail>(
|
|
|
37
37
|
`/api/departments/${deptId.value}`,
|
|
38
38
|
)
|
|
39
39
|
|
|
40
|
+
// PR97a v3.59.0 — 30d activity sparkline for this department.
|
|
41
|
+
interface SparklineDay {
|
|
42
|
+
date: string
|
|
43
|
+
calls: number
|
|
44
|
+
cost_usd: number | null
|
|
45
|
+
}
|
|
46
|
+
const { data: sparklineData } = fetchApi<{
|
|
47
|
+
days: SparklineDay[]
|
|
48
|
+
period_days: number
|
|
49
|
+
department: string
|
|
50
|
+
}>(() => deptId.value ? `/api/departments/${deptId.value}/activity-sparkline?days=30` : '')
|
|
51
|
+
const sparkline = computed<SparklineDay[]>(() => sparklineData.value?.days ?? [])
|
|
52
|
+
const sparklineMaxCalls = computed(() =>
|
|
53
|
+
Math.max(1, sparkline.value.reduce((acc, d) => Math.max(acc, d.calls), 0)),
|
|
54
|
+
)
|
|
55
|
+
const sparklineTotalCalls = computed(() =>
|
|
56
|
+
sparkline.value.reduce((acc, d) => acc + d.calls, 0),
|
|
57
|
+
)
|
|
58
|
+
|
|
40
59
|
// PR90b v3.32.0 — Compare with another department.
|
|
41
60
|
const { data: deptListData } = fetchApi<{ departments: Array<{ department: string }> }>(
|
|
42
61
|
'/api/departments',
|
|
@@ -177,6 +196,39 @@ const tierColor = (tier: number | undefined) => {
|
|
|
177
196
|
:on-retry="() => refresh()"
|
|
178
197
|
>
|
|
179
198
|
<div v-if="detail" class="space-y-5 max-w-5xl">
|
|
199
|
+
<!-- PR97a v3.59.0 — 30d sparkline -->
|
|
200
|
+
<section
|
|
201
|
+
v-if="sparklineTotalCalls > 0"
|
|
202
|
+
class="rounded-xl border border-default bg-elevated/10 p-5"
|
|
203
|
+
>
|
|
204
|
+
<div class="flex items-center justify-between text-xs mb-2">
|
|
205
|
+
<span class="font-semibold text-muted uppercase tracking-wide">
|
|
206
|
+
Daily calls (30d)
|
|
207
|
+
</span>
|
|
208
|
+
<span class="font-mono text-muted">
|
|
209
|
+
{{ sparklineTotalCalls }} total · max {{ sparklineMaxCalls }}/day
|
|
210
|
+
</span>
|
|
211
|
+
</div>
|
|
212
|
+
<svg
|
|
213
|
+
:viewBox="`0 0 ${sparkline.length * 8} 48`"
|
|
214
|
+
class="w-full h-12"
|
|
215
|
+
preserveAspectRatio="none"
|
|
216
|
+
>
|
|
217
|
+
<rect
|
|
218
|
+
v-for="(day, idx) in sparkline"
|
|
219
|
+
:key="day.date"
|
|
220
|
+
:x="idx * 8 + 1"
|
|
221
|
+
:y="48 - (day.calls / sparklineMaxCalls) * 46"
|
|
222
|
+
width="6"
|
|
223
|
+
:height="(day.calls / sparklineMaxCalls) * 46"
|
|
224
|
+
class="fill-primary"
|
|
225
|
+
:class="day.calls === 0 ? 'opacity-20' : 'opacity-90'"
|
|
226
|
+
>
|
|
227
|
+
<title>{{ day.date }} · {{ day.calls }} calls</title>
|
|
228
|
+
</rect>
|
|
229
|
+
</svg>
|
|
230
|
+
</section>
|
|
231
|
+
|
|
180
232
|
<!-- Stats row -->
|
|
181
233
|
<section class="grid grid-cols-2 md:grid-cols-4 gap-3">
|
|
182
234
|
<div class="rounded-xl border border-default p-4 bg-elevated/20">
|
|
@@ -226,6 +226,19 @@ function csvToList(value: string): string[] {
|
|
|
226
226
|
type SuggestField = 'mental_models' | 'frameworks' | 'expertise_domains' | 'communication_avoid' | 'key_quotes'
|
|
227
227
|
const suggestingField = ref<SuggestField | null>(null)
|
|
228
228
|
|
|
229
|
+
// PR97b v3.60.0 — weekly usage timeline (when did agents clone from here).
|
|
230
|
+
interface UsageWeek { week_start: string, count: number }
|
|
231
|
+
const { data: usageTimelineData } = fetchApi<{
|
|
232
|
+
weeks: UsageWeek[]
|
|
233
|
+
total_agents: number
|
|
234
|
+
period_weeks: number
|
|
235
|
+
}>(`/api/personas/${personaId}/usage-timeline?weeks=12`)
|
|
236
|
+
const usageWeeks = computed<UsageWeek[]>(() => usageTimelineData.value?.weeks ?? [])
|
|
237
|
+
const usageMaxCount = computed(() =>
|
|
238
|
+
Math.max(1, usageWeeks.value.reduce((acc, w) => Math.max(acc, w.count), 0)),
|
|
239
|
+
)
|
|
240
|
+
const usageTotalLinks = computed(() => usageTimelineData.value?.total_agents ?? 0)
|
|
241
|
+
|
|
229
242
|
// PR86a v3.15.0 — favorites.
|
|
230
243
|
const favs = useFavorites()
|
|
231
244
|
await favs.load()
|
|
@@ -650,6 +663,44 @@ const vocabOptions = [
|
|
|
650
663
|
</div>
|
|
651
664
|
</section>
|
|
652
665
|
|
|
666
|
+
<!-- PR97b v3.60.0 — usage timeline (when agents linked) -->
|
|
667
|
+
<section
|
|
668
|
+
v-if="usageTotalLinks > 0"
|
|
669
|
+
class="rounded-xl border border-default bg-elevated/10 p-5"
|
|
670
|
+
>
|
|
671
|
+
<div class="flex items-center justify-between text-xs mb-2">
|
|
672
|
+
<span class="font-semibold text-muted uppercase tracking-wide">
|
|
673
|
+
Usage timeline (12 weeks)
|
|
674
|
+
</span>
|
|
675
|
+
<span class="font-mono text-muted">
|
|
676
|
+
{{ usageTotalLinks }} agent{{ usageTotalLinks === 1 ? '' : 's' }} linking · peak {{ usageMaxCount }}/wk
|
|
677
|
+
</span>
|
|
678
|
+
</div>
|
|
679
|
+
<svg
|
|
680
|
+
:viewBox="`0 0 ${usageWeeks.length * 16} 48`"
|
|
681
|
+
class="w-full h-12"
|
|
682
|
+
preserveAspectRatio="none"
|
|
683
|
+
>
|
|
684
|
+
<rect
|
|
685
|
+
v-for="(w, idx) in usageWeeks"
|
|
686
|
+
:key="w.week_start"
|
|
687
|
+
:x="idx * 16 + 2"
|
|
688
|
+
:y="48 - (w.count / usageMaxCount) * 46"
|
|
689
|
+
width="12"
|
|
690
|
+
:height="(w.count / usageMaxCount) * 46"
|
|
691
|
+
class="fill-primary"
|
|
692
|
+
:class="w.count === 0 ? 'opacity-20' : 'opacity-90'"
|
|
693
|
+
>
|
|
694
|
+
<title>{{ w.week_start }} · {{ w.count }} agent{{ w.count === 1 ? '' : 's' }} linked</title>
|
|
695
|
+
</rect>
|
|
696
|
+
</svg>
|
|
697
|
+
<p class="text-xs text-muted mt-1.5">
|
|
698
|
+
Buckets reflect the YAML mtime of agents that currently link to
|
|
699
|
+
this persona — approximation of when they were cloned / edited
|
|
700
|
+
to depend on this profile.
|
|
701
|
+
</p>
|
|
702
|
+
</section>
|
|
703
|
+
|
|
653
704
|
<!-- BIO (PR86d) -->
|
|
654
705
|
<section
|
|
655
706
|
v-if="(detail as any).bio_md"
|
package/package.json
CHANGED
package/pyproject.toml
CHANGED
|
Binary file
|
package/scripts/dashboard-api.py
CHANGED
|
@@ -1371,6 +1371,74 @@ def _obsidian_store_available() -> bool:
|
|
|
1371
1371
|
return False
|
|
1372
1372
|
|
|
1373
1373
|
|
|
1374
|
+
@app.get("/api/personas/{persona_id}/usage-timeline")
|
|
1375
|
+
def persona_usage_timeline(persona_id: str, weeks: int = 12):
|
|
1376
|
+
"""PR97b v3.60.0 — histogram of agent YAML mtimes for agents that
|
|
1377
|
+
link to this persona. Approximation of "when did people clone /
|
|
1378
|
+
create agents from this persona over time".
|
|
1379
|
+
|
|
1380
|
+
Returns ``{weeks: [{week_start, count}], total_agents, period_weeks}``
|
|
1381
|
+
bucketed by ISO week start (Monday). Capped at 52 weeks.
|
|
1382
|
+
|
|
1383
|
+
Uses filesystem mtime for the agent YAML — works even without a
|
|
1384
|
+
git history.
|
|
1385
|
+
"""
|
|
1386
|
+
try:
|
|
1387
|
+
weeks_int = int(weeks) if weeks is not None else 12
|
|
1388
|
+
except (TypeError, ValueError):
|
|
1389
|
+
weeks_int = 12
|
|
1390
|
+
capped_weeks = max(1, min(weeks_int, 52))
|
|
1391
|
+
|
|
1392
|
+
try:
|
|
1393
|
+
import yaml as _yaml
|
|
1394
|
+
except ImportError:
|
|
1395
|
+
return {"weeks": [], "total_agents": 0, "period_weeks": capped_weeks}
|
|
1396
|
+
|
|
1397
|
+
dept_root = ARKAOS_ROOT / "departments"
|
|
1398
|
+
if not dept_root.exists():
|
|
1399
|
+
return {"weeks": [], "total_agents": 0, "period_weeks": capped_weeks}
|
|
1400
|
+
|
|
1401
|
+
from datetime import datetime, timedelta, timezone
|
|
1402
|
+
now = datetime.now(timezone.utc)
|
|
1403
|
+
today = now.date()
|
|
1404
|
+
# Monday of current ISO week.
|
|
1405
|
+
current_monday = today - timedelta(days=today.weekday())
|
|
1406
|
+
buckets: dict[str, int] = {}
|
|
1407
|
+
for offset in range(capped_weeks):
|
|
1408
|
+
m = current_monday - timedelta(weeks=capped_weeks - 1 - offset)
|
|
1409
|
+
buckets[m.isoformat()] = 0
|
|
1410
|
+
cutoff_monday = current_monday - timedelta(weeks=capped_weeks - 1)
|
|
1411
|
+
|
|
1412
|
+
linked_total = 0
|
|
1413
|
+
for path in dept_root.glob("*/agents/*.yaml"):
|
|
1414
|
+
try:
|
|
1415
|
+
raw = _yaml.safe_load(path.read_text(encoding="utf-8")) or {}
|
|
1416
|
+
except Exception: # noqa: BLE001
|
|
1417
|
+
continue
|
|
1418
|
+
if not isinstance(raw, dict):
|
|
1419
|
+
continue
|
|
1420
|
+
linked = raw.get("linked_personas") or []
|
|
1421
|
+
if not isinstance(linked, list) or persona_id not in linked:
|
|
1422
|
+
continue
|
|
1423
|
+
linked_total += 1
|
|
1424
|
+
try:
|
|
1425
|
+
mtime = datetime.fromtimestamp(path.stat().st_mtime, tz=timezone.utc).date()
|
|
1426
|
+
except OSError:
|
|
1427
|
+
continue
|
|
1428
|
+
if mtime < cutoff_monday:
|
|
1429
|
+
continue
|
|
1430
|
+
monday = mtime - timedelta(days=mtime.weekday())
|
|
1431
|
+
key = monday.isoformat()
|
|
1432
|
+
if key in buckets:
|
|
1433
|
+
buckets[key] += 1
|
|
1434
|
+
|
|
1435
|
+
out = [
|
|
1436
|
+
{"week_start": k, "count": buckets[k]}
|
|
1437
|
+
for k in sorted(buckets.keys())
|
|
1438
|
+
]
|
|
1439
|
+
return {"weeks": out, "total_agents": linked_total, "period_weeks": capped_weeks}
|
|
1440
|
+
|
|
1441
|
+
|
|
1374
1442
|
@app.get("/api/personas/usage")
|
|
1375
1443
|
def personas_usage():
|
|
1376
1444
|
"""PR77 v2.95.0 — reverse lookup: how many agents link to each
|
|
@@ -1941,6 +2009,80 @@ def department_merge(src: str, dst: str):
|
|
|
1941
2009
|
}
|
|
1942
2010
|
|
|
1943
2011
|
|
|
2012
|
+
@app.get("/api/departments/{dept_id}/activity-sparkline")
|
|
2013
|
+
def department_activity_sparkline(dept_id: str, days: int = 30):
|
|
2014
|
+
"""PR97a v3.59.0 — daily calls/cost series for a department.
|
|
2015
|
+
|
|
2016
|
+
Returns ``{days: [{date, calls, cost_usd}], period_days, department}``
|
|
2017
|
+
pre-seeded with zeros so the SVG renders without gaps. Counts every
|
|
2018
|
+
telemetry row with ``subagent:<dept>`` or ``subagent:<dept>:<agent>``
|
|
2019
|
+
category. Capped at 90 days.
|
|
2020
|
+
"""
|
|
2021
|
+
dept_id = dept_id.strip().lower()
|
|
2022
|
+
try:
|
|
2023
|
+
days_int = int(days) if days is not None else 30
|
|
2024
|
+
except (TypeError, ValueError):
|
|
2025
|
+
days_int = 30
|
|
2026
|
+
capped_days = max(1, min(days_int, 90))
|
|
2027
|
+
|
|
2028
|
+
# Bail early if department has no agents — keep parity with detail endpoint.
|
|
2029
|
+
if not any(a.get("department") == dept_id for a in _load_agents()):
|
|
2030
|
+
return {"error": "Department not found or has no agents"}
|
|
2031
|
+
|
|
2032
|
+
try:
|
|
2033
|
+
from core.runtime.llm_cost_telemetry import read_entries
|
|
2034
|
+
except Exception:
|
|
2035
|
+
return {"days": []}
|
|
2036
|
+
|
|
2037
|
+
from datetime import datetime, timedelta, timezone
|
|
2038
|
+
today = datetime.now(timezone.utc).date()
|
|
2039
|
+
buckets: dict[str, dict] = {}
|
|
2040
|
+
for offset in range(capped_days):
|
|
2041
|
+
d = today - timedelta(days=capped_days - 1 - offset)
|
|
2042
|
+
buckets[d.isoformat()] = {
|
|
2043
|
+
"date": d.isoformat(),
|
|
2044
|
+
"calls": 0,
|
|
2045
|
+
"cost_usd": 0.0,
|
|
2046
|
+
"cost_known": False,
|
|
2047
|
+
}
|
|
2048
|
+
cutoff = today - timedelta(days=capped_days - 1)
|
|
2049
|
+
prefix_dept = f"subagent:{dept_id}"
|
|
2050
|
+
for entry in read_entries():
|
|
2051
|
+
raw_ts = entry.get("ts") or ""
|
|
2052
|
+
if not isinstance(raw_ts, str):
|
|
2053
|
+
continue
|
|
2054
|
+
try:
|
|
2055
|
+
ts = datetime.fromisoformat(raw_ts.replace("Z", "+00:00"))
|
|
2056
|
+
except ValueError:
|
|
2057
|
+
continue
|
|
2058
|
+
if ts.date() < cutoff:
|
|
2059
|
+
continue
|
|
2060
|
+
cat = str(entry.get("category") or "")
|
|
2061
|
+
# subagent:<dept> OR subagent:<dept>:<agent>
|
|
2062
|
+
if cat != prefix_dept and not cat.startswith(prefix_dept + ":"):
|
|
2063
|
+
continue
|
|
2064
|
+
key = ts.date().isoformat()
|
|
2065
|
+
if key not in buckets:
|
|
2066
|
+
continue
|
|
2067
|
+
buckets[key]["calls"] += 1
|
|
2068
|
+
cost = entry.get("estimated_cost_usd")
|
|
2069
|
+
if isinstance(cost, (int, float)):
|
|
2070
|
+
buckets[key]["cost_usd"] += float(cost)
|
|
2071
|
+
buckets[key]["cost_known"] = True
|
|
2072
|
+
|
|
2073
|
+
out: list[dict] = []
|
|
2074
|
+
for k in sorted(buckets.keys()):
|
|
2075
|
+
bucket = buckets[k]
|
|
2076
|
+
out.append({
|
|
2077
|
+
"date": bucket["date"],
|
|
2078
|
+
"calls": bucket["calls"],
|
|
2079
|
+
"cost_usd": (
|
|
2080
|
+
round(bucket["cost_usd"], 6) if bucket["cost_known"] else None
|
|
2081
|
+
),
|
|
2082
|
+
})
|
|
2083
|
+
return {"days": out, "period_days": capped_days, "department": dept_id}
|
|
2084
|
+
|
|
2085
|
+
|
|
1944
2086
|
@app.get("/api/departments/{dept_id}")
|
|
1945
2087
|
def department_detail(dept_id: str):
|
|
1946
2088
|
"""Full department detail: agents, workflows, 30d cost."""
|