arkaos 3.5.0 → 3.6.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.5.0
1
+ 3.6.0
@@ -27,6 +27,38 @@ const deptActivity = computed<ActivityRow | null>(() =>
27
27
  (activityData.value?.by_department?.[agent.value?.department ?? ''] ?? null),
28
28
  )
29
29
 
30
+ // PR83d v3.6.0 — activity strip (30d, dept-level + last_used + rank)
31
+ interface ActivityStrip {
32
+ period: string
33
+ department: string
34
+ calls: number
35
+ cost_usd: number | null
36
+ tokens_in: number
37
+ tokens_out: number
38
+ last_used: string | null
39
+ dept_rank: number | null
40
+ dept_count: number
41
+ }
42
+ const { data: activityStrip } = fetchApi<ActivityStrip>(
43
+ `/api/agents/${agentId}/activity-strip?period=month`,
44
+ )
45
+
46
+ function formatRelative(iso: string | null): string {
47
+ if (!iso) return 'never'
48
+ const ts = Date.parse(iso)
49
+ if (Number.isNaN(ts)) return 'never'
50
+ const diff = Date.now() - ts
51
+ const minutes = Math.floor(diff / 60_000)
52
+ if (minutes < 1) return 'just now'
53
+ if (minutes < 60) return `${minutes}m ago`
54
+ const hours = Math.floor(minutes / 60)
55
+ if (hours < 24) return `${hours}h ago`
56
+ const days = Math.floor(hours / 24)
57
+ if (days < 30) return `${days}d ago`
58
+ const months = Math.floor(days / 30)
59
+ return `${months}mo ago`
60
+ }
61
+
30
62
  // PR76 — edit drawer state
31
63
  const editOpen = ref(false)
32
64
 
@@ -291,6 +323,49 @@ function formatTokens(n: number): string {
291
323
  </div>
292
324
  </section>
293
325
 
326
+ <!-- ===== ACTIVITY STRIP (PR83d) ===== -->
327
+ <section
328
+ v-if="activityStrip"
329
+ class="rounded-xl border border-default bg-elevated/10 p-4"
330
+ >
331
+ <div class="flex flex-wrap items-center gap-x-6 gap-y-3 text-sm">
332
+ <div class="flex items-center gap-2">
333
+ <UIcon name="i-lucide-activity" class="size-4 text-primary" />
334
+ <span class="font-semibold uppercase tracking-wide text-muted text-xs">
335
+ 30d activity (dept)
336
+ </span>
337
+ </div>
338
+ <div class="flex items-center gap-2">
339
+ <span class="text-muted">Calls</span>
340
+ <span class="font-mono font-semibold">{{ activityStrip.calls }}</span>
341
+ </div>
342
+ <div class="flex items-center gap-2">
343
+ <span class="text-muted">Cost</span>
344
+ <span class="font-mono font-semibold">{{ formatCost(activityStrip.cost_usd) }}</span>
345
+ </div>
346
+ <div class="flex items-center gap-2">
347
+ <span class="text-muted">Tokens</span>
348
+ <span class="font-mono">
349
+ {{ formatTokens(activityStrip.tokens_in) }} /
350
+ {{ formatTokens(activityStrip.tokens_out) }}
351
+ </span>
352
+ </div>
353
+ <div class="flex items-center gap-2">
354
+ <span class="text-muted">Last used</span>
355
+ <span class="font-mono">{{ formatRelative(activityStrip.last_used) }}</span>
356
+ </div>
357
+ <div v-if="activityStrip.dept_rank" class="flex items-center gap-2">
358
+ <span class="text-muted">Dept rank</span>
359
+ <UBadge
360
+ :label="`#${activityStrip.dept_rank} of ${activityStrip.dept_count}`"
361
+ :color="activityStrip.dept_rank <= 3 ? 'primary' : 'neutral'"
362
+ variant="subtle"
363
+ size="sm"
364
+ />
365
+ </div>
366
+ </div>
367
+ </section>
368
+
294
369
  <AgentEditDrawer
295
370
  v-model="editOpen"
296
371
  :agent="agent"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "arkaos",
3
- "version": "3.5.0",
3
+ "version": "3.6.0",
4
4
  "description": "The Operating System for AI Agent Teams",
5
5
  "type": "module",
6
6
  "bin": {
package/pyproject.toml CHANGED
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "arkaos-core"
3
- version = "3.5.0"
3
+ version = "3.6.0"
4
4
  description = "Core engine for ArkaOS — The Operating System for AI Agent Teams"
5
5
  readme = "README.md"
6
6
  license = {text = "MIT"}
@@ -218,6 +218,90 @@ 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-strip")
222
+ def agent_activity_strip(agent_id: str, period: str = "month"):
223
+ """PR83d v3.6.0 — compact activity payload for the agent hero strip.
224
+
225
+ Returns:
226
+ {
227
+ "period": "month",
228
+ "department": "<dept>",
229
+ "calls": <int>,
230
+ "cost_usd": <float|null>,
231
+ "tokens_in": <int>, "tokens_out": <int>,
232
+ "last_used": "<ISO ts>"|null,
233
+ "dept_rank": <1-based int>|null,
234
+ "dept_count": <int>
235
+ }
236
+
237
+ All values reflect the agent's DEPARTMENT (per-agent attribution
238
+ isn't tracked yet — see PR47 telemetry).
239
+ """
240
+ agents = _load_agents()
241
+ base = None
242
+ for a in agents:
243
+ if a.get("id") == agent_id:
244
+ base = dict(a)
245
+ break
246
+ if not base:
247
+ return {"error": "Agent not found"}
248
+ dept = base.get("department") or ""
249
+ try:
250
+ from core.runtime.llm_cost_telemetry import (
251
+ VALID_PERIODS,
252
+ _load_slice,
253
+ _period_cutoff,
254
+ summarise,
255
+ )
256
+ except Exception:
257
+ return {"error": "telemetry unavailable"}
258
+ if period not in VALID_PERIODS:
259
+ period = "month"
260
+
261
+ summary = summarise(period=period)
262
+ dept_costs: list[tuple[str, float]] = []
263
+ target_row: dict | None = None
264
+ for category, row in (summary.by_category or {}).items():
265
+ if not isinstance(category, str) or not category.startswith("subagent:"):
266
+ continue
267
+ cat_dept = category.split(":", 1)[1] or "unknown"
268
+ cost = row.get("total_cost_usd")
269
+ dept_costs.append((cat_dept, float(cost) if isinstance(cost, (int, float)) else 0.0))
270
+ if cat_dept == dept:
271
+ target_row = row
272
+
273
+ dept_costs.sort(key=lambda t: t[1], reverse=True)
274
+ dept_rank: Optional[int] = None
275
+ for idx, (d, _) in enumerate(dept_costs, start=1):
276
+ if d == dept:
277
+ dept_rank = idx
278
+ break
279
+
280
+ entries, _ = _load_slice(None, _period_cutoff(period, now=None))
281
+ last_used: Optional[str] = None
282
+ for entry in reversed(entries):
283
+ cat = entry.get("category") or ""
284
+ if isinstance(cat, str) and cat == f"subagent:{dept}":
285
+ last_used = entry.get("ts")
286
+ break
287
+
288
+ return {
289
+ "period": period,
290
+ "department": dept,
291
+ "calls": int(target_row.get("call_count", 0)) if target_row else 0,
292
+ "cost_usd": (
293
+ float(target_row.get("total_cost_usd"))
294
+ if target_row and isinstance(target_row.get("total_cost_usd"), (int, float))
295
+ else None
296
+ ),
297
+ "tokens_in": int(target_row.get("total_tokens_in", 0)) if target_row else 0,
298
+ "tokens_out": int(target_row.get("total_tokens_out", 0)) if target_row else 0,
299
+ "last_used": last_used,
300
+ "dept_rank": dept_rank,
301
+ "dept_count": len(dept_costs),
302
+ }
303
+
304
+
221
305
  @app.get("/api/agents/{agent_id}")
222
306
  def agent_detail(agent_id: str):
223
307
  """Get full agent detail including YAML data."""