arkaos 3.56.0 → 3.58.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.56.0
1
+ 3.58.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) ===== -->
@@ -283,15 +283,28 @@ function onCloned(agentId: string) {
283
283
  }
284
284
 
285
285
  // PR88a v3.23.0 — Compare with linked agent.
286
- const compareWithOptions = computed(() =>
287
- linkedAgentIds.value.map((aid) => ({
288
- label: `Compare with ${aid}`,
289
- icon: 'i-lucide-columns-2',
286
+ // PR96c v3.57.0 also compare with another persona.
287
+ const { data: otherPersonasData } = fetchApi<{ personas: Array<{ id: string, name: string }> }>(
288
+ '/api/personas',
289
+ )
290
+ const compareWithOptions = computed(() => {
291
+ const agentOpts = linkedAgentIds.value.map((aid) => ({
292
+ label: `Compare with agent ${aid}`,
293
+ icon: 'i-lucide-user',
290
294
  onSelect: () => navigateTo(
291
295
  `/personas/compare-with-agent?persona=${personaId}&agent=${aid}`,
292
296
  ),
293
- })),
294
- )
297
+ }))
298
+ const personaOpts = (otherPersonasData.value?.personas ?? [])
299
+ .filter((p) => p.id !== personaId)
300
+ .slice(0, 30)
301
+ .map((p) => ({
302
+ label: `Compare with persona ${p.name}`,
303
+ icon: 'i-lucide-user-plus',
304
+ onSelect: () => navigateTo(`/personas/compare?a=${personaId}&b=${p.id}`),
305
+ }))
306
+ return [...agentOpts, ...personaOpts]
307
+ })
295
308
 
296
309
  // PR84c v3.9.0 — Auto-fill empty lists in one go.
297
310
  const autofilling = ref(false)
@@ -0,0 +1,213 @@
1
+ <script setup lang="ts">
2
+ // PR96c v3.57.0 — Compare two personas side-by-side.
3
+ //
4
+ // Driven by `?a=p1&b=p2`. Mirrors the agents/compare layout but
5
+ // adapts to the persona schema (flat mental_models, no department).
6
+
7
+ const route = useRoute()
8
+ const { fetchApi } = useApi()
9
+
10
+ const ids = computed<string[]>(() => {
11
+ const raw = [route.query.a, route.query.b]
12
+ return raw.map((v) => String(v ?? '').trim()).filter(Boolean).slice(0, 2)
13
+ })
14
+
15
+ interface PersonaDetail {
16
+ id: string
17
+ name?: string
18
+ title?: string
19
+ source?: string
20
+ tagline?: string
21
+ mbti?: string
22
+ disc?: { primary?: string, secondary?: string }
23
+ enneagram?: { type?: number, wing?: number }
24
+ big_five?: {
25
+ openness?: number
26
+ conscientiousness?: number
27
+ extraversion?: number
28
+ agreeableness?: number
29
+ neuroticism?: number
30
+ }
31
+ mental_models?: string[]
32
+ expertise_domains?: string[]
33
+ frameworks?: string[]
34
+ key_quotes?: string[]
35
+ communication?: { tone?: string, vocabulary_level?: string, avoid?: string[] }
36
+ bio_md?: string
37
+ error?: string
38
+ }
39
+
40
+ const { data: a, status: aStatus } = fetchApi<PersonaDetail>(
41
+ () => ids.value[0] ? `/api/personas/${ids.value[0]}` : '',
42
+ )
43
+ const { data: b, status: bStatus } = fetchApi<PersonaDetail>(
44
+ () => ids.value[1] ? `/api/personas/${ids.value[1]}` : '',
45
+ )
46
+
47
+ const loading = computed(() => aStatus.value === 'pending' || bStatus.value === 'pending')
48
+ const errorMsg = computed(() => {
49
+ if (ids.value.length < 2) return 'Pass two persona ids via ?a=p1&b=p2'
50
+ if (a.value?.error) return `Left: ${a.value.error}`
51
+ if (b.value?.error) return `Right: ${b.value.error}`
52
+ return null
53
+ })
54
+
55
+ function diffClass(left: unknown, right: unknown): string {
56
+ return left !== right ? 'bg-yellow-500/10 border-yellow-500/30' : ''
57
+ }
58
+ function listDiffClass(left: unknown[] | undefined, right: unknown[] | undefined): string {
59
+ const x = JSON.stringify([...(left ?? [])].sort())
60
+ const y = JSON.stringify([...(right ?? [])].sort())
61
+ return x !== y ? 'bg-yellow-500/10 border-yellow-500/30' : ''
62
+ }
63
+
64
+ const bigFiveKeys = ['openness', 'conscientiousness', 'extraversion', 'agreeableness', 'neuroticism'] as const
65
+ </script>
66
+
67
+ <template>
68
+ <UDashboardPanel id="personas-compare">
69
+ <template #header>
70
+ <UDashboardNavbar title="Compare personas">
71
+ <template #leading>
72
+ <UButton icon="i-lucide-arrow-left" variant="ghost" size="sm" to="/personas" aria-label="Back" />
73
+ </template>
74
+ <template #trailing>
75
+ <UBadge label="2-way" variant="subtle" size="sm" />
76
+ </template>
77
+ </UDashboardNavbar>
78
+ </template>
79
+
80
+ <template #body>
81
+ <div v-if="errorMsg" class="p-6 text-center text-sm text-error">
82
+ {{ errorMsg }}
83
+ </div>
84
+ <div v-else-if="loading" class="p-6 text-center text-sm text-muted">
85
+ <UIcon name="i-lucide-loader-2" class="size-4 animate-spin inline" /> Loading…
86
+ </div>
87
+ <div v-else-if="a && b" class="space-y-4 max-w-6xl">
88
+ <section class="grid grid-cols-2 gap-3">
89
+ <NuxtLink :to="`/personas/${a.id}`" class="rounded-lg border border-default p-4 hover:border-primary/40">
90
+ <p class="text-xs text-muted uppercase tracking-wide">Left</p>
91
+ <h2 class="text-xl font-bold">{{ a.name }}</h2>
92
+ <p class="text-sm text-muted">{{ a.title || '—' }}</p>
93
+ </NuxtLink>
94
+ <NuxtLink :to="`/personas/${b.id}`" class="rounded-lg border border-default p-4 hover:border-primary/40">
95
+ <p class="text-xs text-muted uppercase tracking-wide">Right</p>
96
+ <h2 class="text-xl font-bold">{{ b.name }}</h2>
97
+ <p class="text-sm text-muted">{{ b.title || '—' }}</p>
98
+ </NuxtLink>
99
+ </section>
100
+
101
+ <h3 class="text-sm font-semibold uppercase tracking-wide text-muted pt-2">Behavioural DNA</h3>
102
+ <div class="grid grid-cols-2 gap-3">
103
+ <div :class="['rounded-lg border p-3', diffClass(a.mbti, b.mbti)]">
104
+ <p class="text-xs text-muted">MBTI</p>
105
+ <p class="text-lg font-mono font-bold">{{ a.mbti ?? '—' }}</p>
106
+ </div>
107
+ <div :class="['rounded-lg border p-3', diffClass(a.mbti, b.mbti)]">
108
+ <p class="text-xs text-muted">MBTI</p>
109
+ <p class="text-lg font-mono font-bold">{{ b.mbti ?? '—' }}</p>
110
+ </div>
111
+ <div :class="['rounded-lg border p-3', diffClass(`${a.disc?.primary}/${a.disc?.secondary}`, `${b.disc?.primary}/${b.disc?.secondary}`)]">
112
+ <p class="text-xs text-muted">DISC</p>
113
+ <p class="text-lg font-mono font-bold">{{ a.disc?.primary ?? '?' }}/{{ a.disc?.secondary ?? '?' }}</p>
114
+ </div>
115
+ <div :class="['rounded-lg border p-3', diffClass(`${a.disc?.primary}/${a.disc?.secondary}`, `${b.disc?.primary}/${b.disc?.secondary}`)]">
116
+ <p class="text-xs text-muted">DISC</p>
117
+ <p class="text-lg font-mono font-bold">{{ b.disc?.primary ?? '?' }}/{{ b.disc?.secondary ?? '?' }}</p>
118
+ </div>
119
+ <div :class="['rounded-lg border p-3', diffClass(`${a.enneagram?.type}w${a.enneagram?.wing}`, `${b.enneagram?.type}w${b.enneagram?.wing}`)]">
120
+ <p class="text-xs text-muted">Enneagram</p>
121
+ <p class="text-lg font-mono font-bold">{{ a.enneagram?.type ?? '?' }}w{{ a.enneagram?.wing ?? '?' }}</p>
122
+ </div>
123
+ <div :class="['rounded-lg border p-3', diffClass(`${a.enneagram?.type}w${a.enneagram?.wing}`, `${b.enneagram?.type}w${b.enneagram?.wing}`)]">
124
+ <p class="text-xs text-muted">Enneagram</p>
125
+ <p class="text-lg font-mono font-bold">{{ b.enneagram?.type ?? '?' }}w{{ b.enneagram?.wing ?? '?' }}</p>
126
+ </div>
127
+ </div>
128
+
129
+ <h3 class="text-sm font-semibold uppercase tracking-wide text-muted pt-2">Big Five (OCEAN)</h3>
130
+ <div class="space-y-1">
131
+ <div v-for="k in bigFiveKeys" :key="k" class="grid grid-cols-2 gap-3">
132
+ <div :class="['rounded-lg border p-2 flex items-center gap-3', diffClass(a.big_five?.[k], b.big_five?.[k])]">
133
+ <span class="text-xs text-muted w-36 shrink-0 capitalize">{{ k }}</span>
134
+ <span class="font-mono text-sm">{{ a.big_five?.[k] ?? '—' }}</span>
135
+ </div>
136
+ <div :class="['rounded-lg border p-2 flex items-center gap-3', diffClass(a.big_five?.[k], b.big_five?.[k])]">
137
+ <span class="text-xs text-muted w-36 shrink-0 capitalize">{{ k }}</span>
138
+ <span class="font-mono text-sm">{{ b.big_five?.[k] ?? '—' }}</span>
139
+ </div>
140
+ </div>
141
+ </div>
142
+
143
+ <h3 class="text-sm font-semibold uppercase tracking-wide text-muted pt-2">Expertise domains</h3>
144
+ <div class="grid grid-cols-2 gap-3">
145
+ <div :class="['rounded-lg border p-3', listDiffClass(a.expertise_domains, b.expertise_domains)]">
146
+ <ul class="list-disc list-inside text-sm space-y-1">
147
+ <li v-for="d in a.expertise_domains" :key="d">{{ d }}</li>
148
+ <li v-if="!a.expertise_domains?.length" class="list-none text-muted italic">none</li>
149
+ </ul>
150
+ </div>
151
+ <div :class="['rounded-lg border p-3', listDiffClass(a.expertise_domains, b.expertise_domains)]">
152
+ <ul class="list-disc list-inside text-sm space-y-1">
153
+ <li v-for="d in b.expertise_domains" :key="d">{{ d }}</li>
154
+ <li v-if="!b.expertise_domains?.length" class="list-none text-muted italic">none</li>
155
+ </ul>
156
+ </div>
157
+ </div>
158
+
159
+ <h3 class="text-sm font-semibold uppercase tracking-wide text-muted pt-2">Mental models</h3>
160
+ <div class="grid grid-cols-2 gap-3">
161
+ <div :class="['rounded-lg border p-3', listDiffClass(a.mental_models, b.mental_models)]">
162
+ <ul class="list-disc list-inside text-sm space-y-1">
163
+ <li v-for="m in a.mental_models" :key="m">{{ m }}</li>
164
+ <li v-if="!a.mental_models?.length" class="list-none text-muted italic">none</li>
165
+ </ul>
166
+ </div>
167
+ <div :class="['rounded-lg border p-3', listDiffClass(a.mental_models, b.mental_models)]">
168
+ <ul class="list-disc list-inside text-sm space-y-1">
169
+ <li v-for="m in b.mental_models" :key="m">{{ m }}</li>
170
+ <li v-if="!b.mental_models?.length" class="list-none text-muted italic">none</li>
171
+ </ul>
172
+ </div>
173
+ </div>
174
+
175
+ <h3 class="text-sm font-semibold uppercase tracking-wide text-muted pt-2">Frameworks</h3>
176
+ <div class="grid grid-cols-2 gap-3">
177
+ <div :class="['rounded-lg border p-3', listDiffClass(a.frameworks, b.frameworks)]">
178
+ <ul class="list-disc list-inside text-sm space-y-1">
179
+ <li v-for="f in a.frameworks" :key="f">{{ f }}</li>
180
+ <li v-if="!a.frameworks?.length" class="list-none text-muted italic">none</li>
181
+ </ul>
182
+ </div>
183
+ <div :class="['rounded-lg border p-3', listDiffClass(a.frameworks, b.frameworks)]">
184
+ <ul class="list-disc list-inside text-sm space-y-1">
185
+ <li v-for="f in b.frameworks" :key="f">{{ f }}</li>
186
+ <li v-if="!b.frameworks?.length" class="list-none text-muted italic">none</li>
187
+ </ul>
188
+ </div>
189
+ </div>
190
+
191
+ <h3 class="text-sm font-semibold uppercase tracking-wide text-muted pt-2">Bio (Markdown)</h3>
192
+ <TextDiff
193
+ :left="a.bio_md || ''"
194
+ :right="b.bio_md || ''"
195
+ :left-label="a.name || a.id"
196
+ :right-label="b.name || b.id"
197
+ />
198
+
199
+ <h3 class="text-sm font-semibold uppercase tracking-wide text-muted pt-2">Communication tone</h3>
200
+ <TextDiff
201
+ :left="a.communication?.tone || ''"
202
+ :right="b.communication?.tone || ''"
203
+ :left-label="a.name || a.id"
204
+ :right-label="b.name || b.id"
205
+ />
206
+
207
+ <p class="text-xs text-muted pt-4 italic">
208
+ Yellow tint = different. Red removed, green added.
209
+ </p>
210
+ </div>
211
+ </template>
212
+ </UDashboardPanel>
213
+ </template>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "arkaos",
3
- "version": "3.56.0",
3
+ "version": "3.58.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.56.0"
3
+ version = "3.58.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,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.