arkaos 3.20.0 → 3.22.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.20.0
1
+ 3.22.0
@@ -0,0 +1,61 @@
1
+ <script setup lang="ts">
2
+ // PR87d v3.22.0 — compact stats card mounted at the bottom of the
3
+ // sidebar. Polls /api/sidebar-stats every 60s.
4
+
5
+ interface SidebarStats {
6
+ agents: number
7
+ personas: number
8
+ departments: number
9
+ today_cost_usd: number | null
10
+ today_calls: number
11
+ }
12
+
13
+ const { fetchApi } = useApi()
14
+ const { data, refresh } = fetchApi<SidebarStats>('/api/sidebar-stats')
15
+
16
+ let timer: ReturnType<typeof setInterval> | null = null
17
+ onMounted(() => {
18
+ timer = setInterval(() => { refresh() }, 60_000)
19
+ })
20
+ onBeforeUnmount(() => {
21
+ if (timer) clearInterval(timer)
22
+ })
23
+
24
+ function formatCost(cost: number | null): string {
25
+ if (cost === null || cost === undefined) return '—'
26
+ if (cost < 0.01) return '<$0.01'
27
+ if (cost < 1) return `$${cost.toFixed(3)}`
28
+ return `$${cost.toFixed(2)}`
29
+ }
30
+ </script>
31
+
32
+ <template>
33
+ <div
34
+ v-if="data"
35
+ class="rounded-lg border border-default bg-elevated/20 p-3 mx-2 mb-2 text-xs space-y-1.5"
36
+ aria-label="Workspace quick stats"
37
+ >
38
+ <div class="flex items-center justify-between">
39
+ <span class="text-muted">Agents</span>
40
+ <span class="font-mono font-semibold">{{ data.agents }}</span>
41
+ </div>
42
+ <div class="flex items-center justify-between">
43
+ <span class="text-muted">Personas</span>
44
+ <span class="font-mono font-semibold">{{ data.personas }}</span>
45
+ </div>
46
+ <div class="flex items-center justify-between">
47
+ <span class="text-muted">Departments</span>
48
+ <span class="font-mono font-semibold">{{ data.departments }}</span>
49
+ </div>
50
+ <div class="border-t border-default/60 mt-2 pt-1.5 flex items-center justify-between">
51
+ <span class="text-muted">Today</span>
52
+ <span class="font-mono font-semibold text-primary">
53
+ {{ formatCost(data.today_cost_usd) }}
54
+ </span>
55
+ </div>
56
+ <div class="flex items-center justify-between text-[10px] text-muted/70">
57
+ <span>{{ data.today_calls }} call{{ data.today_calls === 1 ? '' : 's' }}</span>
58
+ <span>auto · 60s</span>
59
+ </div>
60
+ </div>
61
+ </template>
@@ -121,12 +121,15 @@ const links = [[{
121
121
  popover
122
122
  />
123
123
 
124
+ <!-- PR87d v3.22.0 — quick stats widget above the bottom nav. -->
125
+ <SidebarStatsWidget v-if="!collapsed" class="mt-auto" />
126
+
124
127
  <UNavigationMenu
125
128
  :collapsed="collapsed"
126
129
  :items="links[1]"
127
130
  orientation="vertical"
128
131
  tooltip
129
- class="mt-auto"
132
+ :class="collapsed ? 'mt-auto' : ''"
130
133
  />
131
134
  </template>
132
135
  </UDashboardSidebar>
@@ -0,0 +1,225 @@
1
+ <script setup lang="ts">
2
+ // PR87c v3.21.0 — Compare two agents side-by-side.
3
+ //
4
+ // Driven by `?ids=a,b` query string. Reads both agent payloads via
5
+ // /api/agents/{id} and renders identity / DNA / knowledge / comms in
6
+ // two columns. Cells where the values differ get a subtle warning
7
+ // tint so the operator can spot deltas quickly.
8
+
9
+ const route = useRoute()
10
+ const { fetchApi } = useApi()
11
+
12
+ const ids = computed<string[]>(() => {
13
+ const raw = route.query.ids
14
+ const str = Array.isArray(raw) ? raw.join(',') : (raw ?? '')
15
+ return String(str).split(',').map((s) => s.trim()).filter(Boolean).slice(0, 2)
16
+ })
17
+
18
+ interface AgentDetail {
19
+ id: string
20
+ name?: string
21
+ role?: string
22
+ department?: string
23
+ tier?: number
24
+ model?: string
25
+ mbti?: string
26
+ disc?: { primary?: string, secondary?: string }
27
+ enneagram?: { type?: number, wing?: number }
28
+ big_five?: {
29
+ openness?: number
30
+ conscientiousness?: number
31
+ extraversion?: number
32
+ agreeableness?: number
33
+ neuroticism?: number
34
+ }
35
+ expertise?: { domains?: string[], frameworks?: string[], depth?: string, years_equivalent?: number }
36
+ mental_models?: { primary?: string[], secondary?: string[] }
37
+ communication?: {
38
+ tone?: string
39
+ vocabulary_level?: string
40
+ preferred_format?: string
41
+ language?: string
42
+ }
43
+ }
44
+
45
+ const { data: a, status: aStatus } = fetchApi<AgentDetail>(
46
+ () => ids.value[0] ? `/api/agents/${ids.value[0]}` : '',
47
+ )
48
+ const { data: b, status: bStatus } = fetchApi<AgentDetail>(
49
+ () => ids.value[1] ? `/api/agents/${ids.value[1]}` : '',
50
+ )
51
+
52
+ const loading = computed(() => aStatus.value === 'pending' || bStatus.value === 'pending')
53
+ const errorMsg = computed(() => {
54
+ if (ids.value.length < 2) return 'Pass two agent ids via ?ids=a,b'
55
+ if (a.value && (a.value as any).error) return `Left agent: ${(a.value as any).error}`
56
+ if (b.value && (b.value as any).error) return `Right agent: ${(b.value as any).error}`
57
+ return null
58
+ })
59
+
60
+ function diffClass(left: unknown, right: unknown): string {
61
+ return left !== right
62
+ ? 'bg-yellow-500/10 border-yellow-500/30'
63
+ : ''
64
+ }
65
+
66
+ function listDiffClass(left: unknown[] | undefined, right: unknown[] | undefined): string {
67
+ const a = JSON.stringify([...(left ?? [])].sort())
68
+ const b = JSON.stringify([...(right ?? [])].sort())
69
+ return a !== b ? 'bg-yellow-500/10 border-yellow-500/30' : ''
70
+ }
71
+
72
+ const bigFiveKeys = ['openness', 'conscientiousness', 'extraversion', 'agreeableness', 'neuroticism'] as const
73
+ </script>
74
+
75
+ <template>
76
+ <UDashboardPanel id="agents-compare">
77
+ <template #header>
78
+ <UDashboardNavbar title="Compare agents">
79
+ <template #leading>
80
+ <UButton icon="i-lucide-arrow-left" variant="ghost" size="sm" to="/agents" aria-label="Back" />
81
+ </template>
82
+ <template #trailing>
83
+ <UBadge label="2-way" variant="subtle" size="sm" />
84
+ </template>
85
+ </UDashboardNavbar>
86
+ </template>
87
+
88
+ <template #body>
89
+ <div v-if="errorMsg" class="p-6 text-center text-sm text-error">
90
+ {{ errorMsg }}
91
+ </div>
92
+ <div v-else-if="loading" class="p-6 text-center text-sm text-muted">
93
+ <UIcon name="i-lucide-loader-2" class="size-4 animate-spin inline" /> Loading…
94
+ </div>
95
+ <div v-else-if="a && b" class="space-y-4 max-w-6xl">
96
+ <section class="grid grid-cols-2 gap-3">
97
+ <div class="rounded-lg border border-default p-4">
98
+ <p class="text-xs text-muted">Left</p>
99
+ <h2 class="text-xl font-bold">{{ a.name }}</h2>
100
+ <p class="text-sm text-muted">{{ a.role }} · {{ a.department }}</p>
101
+ </div>
102
+ <div class="rounded-lg border border-default p-4">
103
+ <p class="text-xs text-muted">Right</p>
104
+ <h2 class="text-xl font-bold">{{ b.name }}</h2>
105
+ <p class="text-sm text-muted">{{ b.role }} · {{ b.department }}</p>
106
+ </div>
107
+ </section>
108
+
109
+ <h3 class="text-sm font-semibold uppercase tracking-wide text-muted pt-2">Identity</h3>
110
+ <div class="grid grid-cols-2 gap-3">
111
+ <div :class="['rounded-lg border p-3', diffClass(a.tier, b.tier)]">
112
+ <p class="text-xs text-muted">Tier</p>
113
+ <p class="text-sm font-mono">{{ a.tier ?? '—' }}</p>
114
+ </div>
115
+ <div :class="['rounded-lg border p-3', diffClass(a.tier, b.tier)]">
116
+ <p class="text-xs text-muted">Tier</p>
117
+ <p class="text-sm font-mono">{{ b.tier ?? '—' }}</p>
118
+ </div>
119
+ <div :class="['rounded-lg border p-3', diffClass(a.model, b.model)]">
120
+ <p class="text-xs text-muted">Model</p>
121
+ <p class="text-sm font-mono">{{ a.model ?? '—' }}</p>
122
+ </div>
123
+ <div :class="['rounded-lg border p-3', diffClass(a.model, b.model)]">
124
+ <p class="text-xs text-muted">Model</p>
125
+ <p class="text-sm font-mono">{{ b.model ?? '—' }}</p>
126
+ </div>
127
+ </div>
128
+
129
+ <h3 class="text-sm font-semibold uppercase tracking-wide text-muted pt-2">Behavioural DNA</h3>
130
+ <div class="grid grid-cols-2 gap-3">
131
+ <div :class="['rounded-lg border p-3', diffClass(a.mbti, b.mbti)]">
132
+ <p class="text-xs text-muted">MBTI</p>
133
+ <p class="text-lg font-mono font-bold">{{ a.mbti ?? '—' }}</p>
134
+ </div>
135
+ <div :class="['rounded-lg border p-3', diffClass(a.mbti, b.mbti)]">
136
+ <p class="text-xs text-muted">MBTI</p>
137
+ <p class="text-lg font-mono font-bold">{{ b.mbti ?? '—' }}</p>
138
+ </div>
139
+ <div :class="['rounded-lg border p-3', diffClass(`${a.disc?.primary}/${a.disc?.secondary}`, `${b.disc?.primary}/${b.disc?.secondary}`)]">
140
+ <p class="text-xs text-muted">DISC</p>
141
+ <p class="text-lg font-mono font-bold">{{ a.disc?.primary ?? '?' }}/{{ a.disc?.secondary ?? '?' }}</p>
142
+ </div>
143
+ <div :class="['rounded-lg border p-3', diffClass(`${a.disc?.primary}/${a.disc?.secondary}`, `${b.disc?.primary}/${b.disc?.secondary}`)]">
144
+ <p class="text-xs text-muted">DISC</p>
145
+ <p class="text-lg font-mono font-bold">{{ b.disc?.primary ?? '?' }}/{{ b.disc?.secondary ?? '?' }}</p>
146
+ </div>
147
+ <div :class="['rounded-lg border p-3', diffClass(`${a.enneagram?.type}w${a.enneagram?.wing}`, `${b.enneagram?.type}w${b.enneagram?.wing}`)]">
148
+ <p class="text-xs text-muted">Enneagram</p>
149
+ <p class="text-lg font-mono font-bold">{{ a.enneagram?.type ?? '?' }}w{{ a.enneagram?.wing ?? '?' }}</p>
150
+ </div>
151
+ <div :class="['rounded-lg border p-3', diffClass(`${a.enneagram?.type}w${a.enneagram?.wing}`, `${b.enneagram?.type}w${b.enneagram?.wing}`)]">
152
+ <p class="text-xs text-muted">Enneagram</p>
153
+ <p class="text-lg font-mono font-bold">{{ b.enneagram?.type ?? '?' }}w{{ b.enneagram?.wing ?? '?' }}</p>
154
+ </div>
155
+ </div>
156
+
157
+ <h3 class="text-sm font-semibold uppercase tracking-wide text-muted pt-2">Big Five (OCEAN)</h3>
158
+ <div class="space-y-1">
159
+ <div v-for="k in bigFiveKeys" :key="k" class="grid grid-cols-2 gap-3">
160
+ <div :class="['rounded-lg border p-2 flex items-center gap-3', diffClass(a.big_five?.[k], b.big_five?.[k])]">
161
+ <span class="text-xs text-muted w-36 shrink-0 capitalize">{{ k }}</span>
162
+ <span class="font-mono text-sm">{{ a.big_five?.[k] ?? '—' }}</span>
163
+ </div>
164
+ <div :class="['rounded-lg border p-2 flex items-center gap-3', diffClass(a.big_five?.[k], b.big_five?.[k])]">
165
+ <span class="text-xs text-muted w-36 shrink-0 capitalize">{{ k }}</span>
166
+ <span class="font-mono text-sm">{{ b.big_five?.[k] ?? '—' }}</span>
167
+ </div>
168
+ </div>
169
+ </div>
170
+
171
+ <h3 class="text-sm font-semibold uppercase tracking-wide text-muted pt-2">Expertise domains</h3>
172
+ <div class="grid grid-cols-2 gap-3">
173
+ <div :class="['rounded-lg border p-3', listDiffClass(a.expertise?.domains, b.expertise?.domains)]">
174
+ <ul class="list-disc list-inside text-sm space-y-1">
175
+ <li v-for="d in a.expertise?.domains" :key="d">{{ d }}</li>
176
+ <li v-if="!a.expertise?.domains?.length" class="list-none text-muted italic">none</li>
177
+ </ul>
178
+ </div>
179
+ <div :class="['rounded-lg border p-3', listDiffClass(a.expertise?.domains, b.expertise?.domains)]">
180
+ <ul class="list-disc list-inside text-sm space-y-1">
181
+ <li v-for="d in b.expertise?.domains" :key="d">{{ d }}</li>
182
+ <li v-if="!b.expertise?.domains?.length" class="list-none text-muted italic">none</li>
183
+ </ul>
184
+ </div>
185
+ </div>
186
+
187
+ <h3 class="text-sm font-semibold uppercase tracking-wide text-muted pt-2">Frameworks</h3>
188
+ <div class="grid grid-cols-2 gap-3">
189
+ <div :class="['rounded-lg border p-3', listDiffClass(a.expertise?.frameworks, b.expertise?.frameworks)]">
190
+ <ul class="list-disc list-inside text-sm space-y-1">
191
+ <li v-for="f in a.expertise?.frameworks" :key="f">{{ f }}</li>
192
+ <li v-if="!a.expertise?.frameworks?.length" class="list-none text-muted italic">none</li>
193
+ </ul>
194
+ </div>
195
+ <div :class="['rounded-lg border p-3', listDiffClass(a.expertise?.frameworks, b.expertise?.frameworks)]">
196
+ <ul class="list-disc list-inside text-sm space-y-1">
197
+ <li v-for="f in b.expertise?.frameworks" :key="f">{{ f }}</li>
198
+ <li v-if="!b.expertise?.frameworks?.length" class="list-none text-muted italic">none</li>
199
+ </ul>
200
+ </div>
201
+ </div>
202
+
203
+ <h3 class="text-sm font-semibold uppercase tracking-wide text-muted pt-2">Mental models (primary)</h3>
204
+ <div class="grid grid-cols-2 gap-3">
205
+ <div :class="['rounded-lg border p-3', listDiffClass(a.mental_models?.primary, b.mental_models?.primary)]">
206
+ <ul class="list-disc list-inside text-sm space-y-1">
207
+ <li v-for="m in a.mental_models?.primary" :key="m">{{ m }}</li>
208
+ <li v-if="!a.mental_models?.primary?.length" class="list-none text-muted italic">none</li>
209
+ </ul>
210
+ </div>
211
+ <div :class="['rounded-lg border p-3', listDiffClass(a.mental_models?.primary, b.mental_models?.primary)]">
212
+ <ul class="list-disc list-inside text-sm space-y-1">
213
+ <li v-for="m in b.mental_models?.primary" :key="m">{{ m }}</li>
214
+ <li v-if="!b.mental_models?.primary?.length" class="list-none text-muted italic">none</li>
215
+ </ul>
216
+ </div>
217
+ </div>
218
+
219
+ <p class="text-xs text-muted pt-4 italic">
220
+ Cells with a yellow tint differ between the two agents.
221
+ </p>
222
+ </div>
223
+ </template>
224
+ </UDashboardPanel>
225
+ </template>
@@ -220,6 +220,12 @@ const departmentMoveOptions = [
220
220
  onSelect: () => bulkMove(d),
221
221
  }))
222
222
 
223
+ function openCompare() {
224
+ if (selected.value.size !== 2) return
225
+ const ids = Array.from(selected.value).slice(0, 2).join(',')
226
+ navigateTo(`/agents/compare?ids=${ids}`)
227
+ }
228
+
223
229
  async function bulkMove(targetDept: string) {
224
230
  if (selected.value.size === 0) return
225
231
  const ids = Array.from(selected.value)
@@ -552,6 +558,14 @@ async function undoTrashIds(ids: string[]) {
552
558
  @click="clearSelection"
553
559
  />
554
560
  <div class="h-5 w-px bg-default" />
561
+ <UButton
562
+ label="Compare"
563
+ icon="i-lucide-columns-2"
564
+ size="sm"
565
+ variant="soft"
566
+ :disabled="selected.size !== 2"
567
+ @click="openCompare"
568
+ />
555
569
  <UDropdownMenu :items="departmentMoveOptions">
556
570
  <UButton
557
571
  label="Move to..."
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "arkaos",
3
- "version": "3.20.0",
3
+ "version": "3.22.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.20.0"
3
+ version = "3.22.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"}
@@ -1387,6 +1387,43 @@ def agent_export_to_vault(agent_id: str):
1387
1387
  return {"exported": True, "path": str(res.path), "vault_path": str(res.vault_path)}
1388
1388
 
1389
1389
 
1390
+ # --- Sidebar stats widget (PR87d v3.22.0) ---
1391
+
1392
+ @app.get("/api/sidebar-stats")
1393
+ def sidebar_stats():
1394
+ """Compact payload for the sidebar status widget.
1395
+
1396
+ Returns counts the widget actually displays — agents, personas,
1397
+ departments, today's spend. Cheaper than /api/overview/command-center
1398
+ because it skips project scanning, incidents, and quick actions.
1399
+ """
1400
+ agents = _load_agents()
1401
+ departments = {a.get("department") for a in agents if a.get("department")}
1402
+ persona_count = 0
1403
+ mgr = _get_persona_manager()
1404
+ if mgr:
1405
+ try:
1406
+ persona_count = len(mgr.list() or [])
1407
+ except Exception:
1408
+ persona_count = 0
1409
+ today_cost_usd: float | None = None
1410
+ call_count = 0
1411
+ try:
1412
+ from core.runtime.llm_cost_telemetry import summarise
1413
+ s = summarise(period="today")
1414
+ today_cost_usd = s.total_cost_usd
1415
+ call_count = s.call_count
1416
+ except Exception:
1417
+ pass
1418
+ return {
1419
+ "agents": len(agents),
1420
+ "personas": persona_count,
1421
+ "departments": len(departments),
1422
+ "today_cost_usd": today_cost_usd,
1423
+ "today_calls": call_count,
1424
+ }
1425
+
1426
+
1390
1427
  # --- Favorites (PR86a v3.15.0) ---
1391
1428
 
1392
1429
  @app.get("/api/favorites")