arkaos 3.57.0 → 3.59.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.59.0
|
|
@@ -28,6 +28,26 @@ const deptActivity = computed<ActivityRow | null>(() =>
|
|
|
28
28
|
(activityData.value?.by_department?.[agent.value?.department ?? ''] ?? null),
|
|
29
29
|
)
|
|
30
30
|
|
|
31
|
+
// PR96d v3.58.0 — 30d activity sparkline (calls per day).
|
|
32
|
+
interface SparklineDay {
|
|
33
|
+
date: string
|
|
34
|
+
calls: number
|
|
35
|
+
cost_usd: number | null
|
|
36
|
+
}
|
|
37
|
+
const { data: sparklineData } = fetchApi<{
|
|
38
|
+
days: SparklineDay[]
|
|
39
|
+
period_days: number
|
|
40
|
+
department: string
|
|
41
|
+
}>(`/api/agents/${agentId}/activity-sparkline?days=30`)
|
|
42
|
+
const sparkline = computed<SparklineDay[]>(() => sparklineData.value?.days ?? [])
|
|
43
|
+
const sparklineMaxCalls = computed(() => {
|
|
44
|
+
const max = sparkline.value.reduce((acc, d) => Math.max(acc, d.calls), 0)
|
|
45
|
+
return Math.max(max, 1)
|
|
46
|
+
})
|
|
47
|
+
const sparklineTotalCalls = computed(() =>
|
|
48
|
+
sparkline.value.reduce((acc, d) => acc + d.calls, 0),
|
|
49
|
+
)
|
|
50
|
+
|
|
31
51
|
// PR88d v3.26.0 — agent history (git log + trash entries)
|
|
32
52
|
interface HistoryEvent {
|
|
33
53
|
kind: string
|
|
@@ -558,6 +578,36 @@ function formatTokens(n: number): string {
|
|
|
558
578
|
/>
|
|
559
579
|
</div>
|
|
560
580
|
</div>
|
|
581
|
+
<!-- PR96d v3.58.0 — sparkline of daily calls -->
|
|
582
|
+
<div
|
|
583
|
+
v-if="sparkline.length > 0 && sparklineTotalCalls > 0"
|
|
584
|
+
class="mt-3 pt-3 border-t border-default/60"
|
|
585
|
+
>
|
|
586
|
+
<div class="flex items-center justify-between text-xs mb-1.5">
|
|
587
|
+
<span class="text-muted uppercase tracking-wide">Daily calls</span>
|
|
588
|
+
<span class="font-mono text-muted">
|
|
589
|
+
{{ sparklineTotalCalls }} total · max {{ sparklineMaxCalls }}/day
|
|
590
|
+
</span>
|
|
591
|
+
</div>
|
|
592
|
+
<svg
|
|
593
|
+
:viewBox="`0 0 ${sparkline.length * 6} 32`"
|
|
594
|
+
class="w-full h-8"
|
|
595
|
+
preserveAspectRatio="none"
|
|
596
|
+
>
|
|
597
|
+
<rect
|
|
598
|
+
v-for="(day, idx) in sparkline"
|
|
599
|
+
:key="day.date"
|
|
600
|
+
:x="idx * 6 + 1"
|
|
601
|
+
:y="32 - (day.calls / sparklineMaxCalls) * 30"
|
|
602
|
+
width="4"
|
|
603
|
+
:height="(day.calls / sparklineMaxCalls) * 30"
|
|
604
|
+
class="fill-primary"
|
|
605
|
+
:class="day.calls === 0 ? 'opacity-20' : 'opacity-90'"
|
|
606
|
+
>
|
|
607
|
+
<title>{{ day.date }} · {{ day.calls }} calls</title>
|
|
608
|
+
</rect>
|
|
609
|
+
</svg>
|
|
610
|
+
</div>
|
|
561
611
|
</section>
|
|
562
612
|
|
|
563
613
|
<!-- ===== BIO (PR86d) ===== -->
|
|
@@ -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">
|
package/package.json
CHANGED
package/pyproject.toml
CHANGED
|
Binary file
|
package/scripts/dashboard-api.py
CHANGED
|
@@ -218,6 +218,82 @@ def agents_activity(period: str = "week"):
|
|
|
218
218
|
return {"by_department": out, "period": period}
|
|
219
219
|
|
|
220
220
|
|
|
221
|
+
@app.get("/api/agents/{agent_id}/activity-sparkline")
|
|
222
|
+
def agent_activity_sparkline(agent_id: str, days: int = 30):
|
|
223
|
+
"""PR96d v3.58.0 — daily call / cost series for the agent's department.
|
|
224
|
+
|
|
225
|
+
Returns ``{days: [{date, calls, cost_usd}]}`` seeded with zeros so
|
|
226
|
+
the frontend can render a clean N-day bar chart without gaps.
|
|
227
|
+
Capped at 90 days. Per-agent series falls back to dept series
|
|
228
|
+
(same convention as activity-strip / PR86b).
|
|
229
|
+
"""
|
|
230
|
+
try:
|
|
231
|
+
days_int = int(days) if days is not None else 30
|
|
232
|
+
except (TypeError, ValueError):
|
|
233
|
+
days_int = 30
|
|
234
|
+
capped_days = max(1, min(days_int, 90))
|
|
235
|
+
|
|
236
|
+
agents = _load_agents()
|
|
237
|
+
base = next((a for a in agents if a.get("id") == agent_id), None)
|
|
238
|
+
if not base:
|
|
239
|
+
return {"error": "Agent not found"}
|
|
240
|
+
dept = base.get("department") or ""
|
|
241
|
+
|
|
242
|
+
try:
|
|
243
|
+
from core.runtime.llm_cost_telemetry import read_entries
|
|
244
|
+
except Exception:
|
|
245
|
+
return {"days": []}
|
|
246
|
+
|
|
247
|
+
from datetime import datetime, timedelta, timezone
|
|
248
|
+
today = datetime.now(timezone.utc).date()
|
|
249
|
+
buckets: dict[str, dict] = {}
|
|
250
|
+
for offset in range(capped_days):
|
|
251
|
+
d = today - timedelta(days=capped_days - 1 - offset)
|
|
252
|
+
buckets[d.isoformat()] = {
|
|
253
|
+
"date": d.isoformat(),
|
|
254
|
+
"calls": 0,
|
|
255
|
+
"cost_usd": 0.0,
|
|
256
|
+
"cost_known": False,
|
|
257
|
+
}
|
|
258
|
+
cutoff = today - timedelta(days=capped_days - 1)
|
|
259
|
+
agent_cat = f"subagent:{dept}:{agent_id}"
|
|
260
|
+
dept_cat = f"subagent:{dept}"
|
|
261
|
+
for entry in read_entries():
|
|
262
|
+
raw_ts = entry.get("ts") or ""
|
|
263
|
+
if not isinstance(raw_ts, str):
|
|
264
|
+
continue
|
|
265
|
+
try:
|
|
266
|
+
ts = datetime.fromisoformat(raw_ts.replace("Z", "+00:00"))
|
|
267
|
+
except ValueError:
|
|
268
|
+
continue
|
|
269
|
+
if ts.date() < cutoff:
|
|
270
|
+
continue
|
|
271
|
+
cat = str(entry.get("category") or "")
|
|
272
|
+
if cat != agent_cat and cat != dept_cat:
|
|
273
|
+
continue
|
|
274
|
+
key = ts.date().isoformat()
|
|
275
|
+
if key not in buckets:
|
|
276
|
+
continue
|
|
277
|
+
buckets[key]["calls"] += 1
|
|
278
|
+
cost = entry.get("estimated_cost_usd")
|
|
279
|
+
if isinstance(cost, (int, float)):
|
|
280
|
+
buckets[key]["cost_usd"] += float(cost)
|
|
281
|
+
buckets[key]["cost_known"] = True
|
|
282
|
+
|
|
283
|
+
out: list[dict] = []
|
|
284
|
+
for k in sorted(buckets.keys()):
|
|
285
|
+
bucket = buckets[k]
|
|
286
|
+
out.append({
|
|
287
|
+
"date": bucket["date"],
|
|
288
|
+
"calls": bucket["calls"],
|
|
289
|
+
"cost_usd": (
|
|
290
|
+
round(bucket["cost_usd"], 6)
|
|
291
|
+
if bucket["cost_known"] else None
|
|
292
|
+
),
|
|
293
|
+
})
|
|
294
|
+
return {"days": out, "period_days": capped_days, "department": dept}
|
|
295
|
+
|
|
296
|
+
|
|
221
297
|
@app.get("/api/agents/{agent_id}/activity-strip")
|
|
222
298
|
def agent_activity_strip(agent_id: str, period: str = "month"):
|
|
223
299
|
"""PR83d v3.6.0 + PR86b v3.16.0 — compact activity payload.
|
|
@@ -1865,6 +1941,80 @@ def department_merge(src: str, dst: str):
|
|
|
1865
1941
|
}
|
|
1866
1942
|
|
|
1867
1943
|
|
|
1944
|
+
@app.get("/api/departments/{dept_id}/activity-sparkline")
|
|
1945
|
+
def department_activity_sparkline(dept_id: str, days: int = 30):
|
|
1946
|
+
"""PR97a v3.59.0 — daily calls/cost series for a department.
|
|
1947
|
+
|
|
1948
|
+
Returns ``{days: [{date, calls, cost_usd}], period_days, department}``
|
|
1949
|
+
pre-seeded with zeros so the SVG renders without gaps. Counts every
|
|
1950
|
+
telemetry row with ``subagent:<dept>`` or ``subagent:<dept>:<agent>``
|
|
1951
|
+
category. Capped at 90 days.
|
|
1952
|
+
"""
|
|
1953
|
+
dept_id = dept_id.strip().lower()
|
|
1954
|
+
try:
|
|
1955
|
+
days_int = int(days) if days is not None else 30
|
|
1956
|
+
except (TypeError, ValueError):
|
|
1957
|
+
days_int = 30
|
|
1958
|
+
capped_days = max(1, min(days_int, 90))
|
|
1959
|
+
|
|
1960
|
+
# Bail early if department has no agents — keep parity with detail endpoint.
|
|
1961
|
+
if not any(a.get("department") == dept_id for a in _load_agents()):
|
|
1962
|
+
return {"error": "Department not found or has no agents"}
|
|
1963
|
+
|
|
1964
|
+
try:
|
|
1965
|
+
from core.runtime.llm_cost_telemetry import read_entries
|
|
1966
|
+
except Exception:
|
|
1967
|
+
return {"days": []}
|
|
1968
|
+
|
|
1969
|
+
from datetime import datetime, timedelta, timezone
|
|
1970
|
+
today = datetime.now(timezone.utc).date()
|
|
1971
|
+
buckets: dict[str, dict] = {}
|
|
1972
|
+
for offset in range(capped_days):
|
|
1973
|
+
d = today - timedelta(days=capped_days - 1 - offset)
|
|
1974
|
+
buckets[d.isoformat()] = {
|
|
1975
|
+
"date": d.isoformat(),
|
|
1976
|
+
"calls": 0,
|
|
1977
|
+
"cost_usd": 0.0,
|
|
1978
|
+
"cost_known": False,
|
|
1979
|
+
}
|
|
1980
|
+
cutoff = today - timedelta(days=capped_days - 1)
|
|
1981
|
+
prefix_dept = f"subagent:{dept_id}"
|
|
1982
|
+
for entry in read_entries():
|
|
1983
|
+
raw_ts = entry.get("ts") or ""
|
|
1984
|
+
if not isinstance(raw_ts, str):
|
|
1985
|
+
continue
|
|
1986
|
+
try:
|
|
1987
|
+
ts = datetime.fromisoformat(raw_ts.replace("Z", "+00:00"))
|
|
1988
|
+
except ValueError:
|
|
1989
|
+
continue
|
|
1990
|
+
if ts.date() < cutoff:
|
|
1991
|
+
continue
|
|
1992
|
+
cat = str(entry.get("category") or "")
|
|
1993
|
+
# subagent:<dept> OR subagent:<dept>:<agent>
|
|
1994
|
+
if cat != prefix_dept and not cat.startswith(prefix_dept + ":"):
|
|
1995
|
+
continue
|
|
1996
|
+
key = ts.date().isoformat()
|
|
1997
|
+
if key not in buckets:
|
|
1998
|
+
continue
|
|
1999
|
+
buckets[key]["calls"] += 1
|
|
2000
|
+
cost = entry.get("estimated_cost_usd")
|
|
2001
|
+
if isinstance(cost, (int, float)):
|
|
2002
|
+
buckets[key]["cost_usd"] += float(cost)
|
|
2003
|
+
buckets[key]["cost_known"] = True
|
|
2004
|
+
|
|
2005
|
+
out: list[dict] = []
|
|
2006
|
+
for k in sorted(buckets.keys()):
|
|
2007
|
+
bucket = buckets[k]
|
|
2008
|
+
out.append({
|
|
2009
|
+
"date": bucket["date"],
|
|
2010
|
+
"calls": bucket["calls"],
|
|
2011
|
+
"cost_usd": (
|
|
2012
|
+
round(bucket["cost_usd"], 6) if bucket["cost_known"] else None
|
|
2013
|
+
),
|
|
2014
|
+
})
|
|
2015
|
+
return {"days": out, "period_days": capped_days, "department": dept_id}
|
|
2016
|
+
|
|
2017
|
+
|
|
1868
2018
|
@app.get("/api/departments/{dept_id}")
|
|
1869
2019
|
def department_detail(dept_id: str):
|
|
1870
2020
|
"""Full department detail: agents, workflows, 30d cost."""
|