arkaos 2.85.0 → 2.87.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 +1 -1
- package/dashboard/app/pages/agents/index.vue +97 -28
- package/dashboard/app/pages/health.vue +211 -19
- package/dashboard/app/types/index.d.ts +3 -0
- package/package.json +1 -1
- package/pyproject.toml +1 -1
- package/scripts/__pycache__/dashboard-api.cpython-313.pyc +0 -0
- package/scripts/dashboard-api.py +99 -10
package/VERSION
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
2.
|
|
1
|
+
2.87.0
|
|
@@ -2,12 +2,73 @@
|
|
|
2
2
|
import type { TableColumn } from '@nuxt/ui'
|
|
3
3
|
import type { Agent } from '~/types'
|
|
4
4
|
|
|
5
|
-
const { fetchApi } = useApi()
|
|
5
|
+
const { fetchApi, apiBase } = useApi()
|
|
6
|
+
const toast = useToast()
|
|
6
7
|
|
|
7
8
|
const { data, status, error, refresh } = await fetchApi<{ agents: Agent[], total: number }>('/api/agents')
|
|
8
9
|
|
|
10
|
+
// PR69 v2.86.0 — per-department activity from PR47 telemetry.
|
|
11
|
+
// Used to badge agents whose department has run recently and to
|
|
12
|
+
// surface "no activity yet" hint when a department's never been
|
|
13
|
+
// invoked. Failure-tolerant — returns empty if telemetry unavailable.
|
|
14
|
+
interface ActivityRow {
|
|
15
|
+
call_count: number
|
|
16
|
+
total_cost_usd: number | null
|
|
17
|
+
total_tokens_in: number
|
|
18
|
+
total_tokens_out: number
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const {
|
|
22
|
+
data: activityData,
|
|
23
|
+
refresh: refreshActivity,
|
|
24
|
+
} = fetchApi<{ by_department: Record<string, ActivityRow>, period: string }>(
|
|
25
|
+
'/api/agents/activity?period=week',
|
|
26
|
+
)
|
|
27
|
+
|
|
9
28
|
const agents = computed(() => data.value?.agents ?? [])
|
|
10
29
|
|
|
30
|
+
function deptActivity(dept: string): ActivityRow | undefined {
|
|
31
|
+
return activityData.value?.by_department?.[dept]
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const copied = ref<string | null>(null)
|
|
35
|
+
let copyTimer: ReturnType<typeof setTimeout> | null = null
|
|
36
|
+
|
|
37
|
+
async function copyAgentMention(agent: Agent) {
|
|
38
|
+
if (typeof navigator === 'undefined' || !navigator.clipboard) {
|
|
39
|
+
toast.add({ title: 'Clipboard unavailable', color: 'warning' })
|
|
40
|
+
return
|
|
41
|
+
}
|
|
42
|
+
// The most useful copy for an operator: a ready-to-paste mention
|
|
43
|
+
// that names the agent + their role so the orchestrator can dispatch.
|
|
44
|
+
const text = `Use ${agent.name} (${agent.role}, dept ${agent.department}, tier ${agent.tier}) for this task.`
|
|
45
|
+
try {
|
|
46
|
+
await navigator.clipboard.writeText(text)
|
|
47
|
+
copied.value = agent.id
|
|
48
|
+
if (copyTimer) clearTimeout(copyTimer)
|
|
49
|
+
copyTimer = setTimeout(() => { copied.value = null; copyTimer = null }, 1500)
|
|
50
|
+
toast.add({
|
|
51
|
+
title: 'Copied',
|
|
52
|
+
description: `${agent.name} mention ready to paste.`,
|
|
53
|
+
color: 'success',
|
|
54
|
+
})
|
|
55
|
+
} catch (err) {
|
|
56
|
+
toast.add({
|
|
57
|
+
title: 'Copy failed',
|
|
58
|
+
description: err instanceof Error ? err.message : 'unknown error',
|
|
59
|
+
color: 'error',
|
|
60
|
+
})
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
onBeforeUnmount(() => {
|
|
65
|
+
if (copyTimer) clearTimeout(copyTimer)
|
|
66
|
+
})
|
|
67
|
+
|
|
68
|
+
async function refreshAll() {
|
|
69
|
+
await Promise.all([refresh(), refreshActivity()])
|
|
70
|
+
}
|
|
71
|
+
|
|
11
72
|
const search = ref('')
|
|
12
73
|
const departmentFilter = ref('all')
|
|
13
74
|
const tierFilter = ref('all')
|
|
@@ -77,35 +138,18 @@ const tierColor = (tier: number) => {
|
|
|
77
138
|
}
|
|
78
139
|
|
|
79
140
|
const columns: TableColumn<Agent>[] = [
|
|
80
|
-
{
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
},
|
|
84
|
-
{
|
|
85
|
-
accessorKey: 'role',
|
|
86
|
-
header: 'Role'
|
|
87
|
-
},
|
|
88
|
-
{
|
|
89
|
-
accessorKey: 'department',
|
|
90
|
-
header: 'Department'
|
|
91
|
-
},
|
|
92
|
-
{
|
|
93
|
-
accessorKey: 'tier',
|
|
94
|
-
header: 'Tier'
|
|
95
|
-
},
|
|
141
|
+
{ accessorKey: 'name', header: 'Name' },
|
|
142
|
+
{ accessorKey: 'role', header: 'Role' },
|
|
143
|
+
{ accessorKey: 'department', header: 'Department' },
|
|
144
|
+
{ accessorKey: 'tier', header: 'Tier' },
|
|
96
145
|
{
|
|
97
146
|
accessorFn: (row: Agent) => row.disc?.primary ?? '-',
|
|
98
147
|
id: 'disc',
|
|
99
|
-
header: 'DISC'
|
|
100
|
-
},
|
|
101
|
-
{
|
|
102
|
-
accessorKey: 'mbti',
|
|
103
|
-
header: 'MBTI'
|
|
148
|
+
header: 'DISC',
|
|
104
149
|
},
|
|
105
|
-
{
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
}
|
|
150
|
+
{ accessorKey: 'mbti', header: 'MBTI' },
|
|
151
|
+
{ id: 'activity', header: 'Activity (7d)' },
|
|
152
|
+
{ id: 'actions', header: '' },
|
|
109
153
|
]
|
|
110
154
|
|
|
111
155
|
function goToAgent(id: string) {
|
|
@@ -134,7 +178,7 @@ function goToAgent(id: string) {
|
|
|
134
178
|
empty-title="No agents found"
|
|
135
179
|
empty-icon="i-lucide-users"
|
|
136
180
|
loading-label="Loading agents"
|
|
137
|
-
:on-retry="() =>
|
|
181
|
+
:on-retry="() => refreshAll()"
|
|
138
182
|
>
|
|
139
183
|
<div class="flex flex-wrap items-center gap-3 mb-4">
|
|
140
184
|
<UInput
|
|
@@ -195,8 +239,33 @@ function goToAgent(id: string) {
|
|
|
195
239
|
<template #mbti-cell="{ row }">
|
|
196
240
|
<span class="font-mono text-sm">{{ row.original.mbti || '-' }}</span>
|
|
197
241
|
</template>
|
|
242
|
+
<template #activity-cell="{ row }">
|
|
243
|
+
<template v-if="deptActivity(row.original.department)">
|
|
244
|
+
<div class="flex items-center gap-2">
|
|
245
|
+
<span class="inline-block size-2 rounded-full bg-green-500" />
|
|
246
|
+
<span class="text-xs font-mono">
|
|
247
|
+
{{ deptActivity(row.original.department)?.call_count ?? 0 }} calls
|
|
248
|
+
</span>
|
|
249
|
+
</div>
|
|
250
|
+
</template>
|
|
251
|
+
<span v-else class="text-xs text-muted">—</span>
|
|
252
|
+
</template>
|
|
198
253
|
<template #actions-cell="{ row }">
|
|
199
|
-
<UButton
|
|
254
|
+
<UButton
|
|
255
|
+
:icon="copied === row.original.id ? 'i-lucide-check' : 'i-lucide-copy'"
|
|
256
|
+
:color="copied === row.original.id ? 'success' : 'neutral'"
|
|
257
|
+
variant="ghost"
|
|
258
|
+
size="xs"
|
|
259
|
+
aria-label="Copy agent mention"
|
|
260
|
+
@click.stop="copyAgentMention(row.original)"
|
|
261
|
+
/>
|
|
262
|
+
<UButton
|
|
263
|
+
size="xs"
|
|
264
|
+
variant="ghost"
|
|
265
|
+
icon="i-lucide-arrow-right"
|
|
266
|
+
aria-label="Open agent detail"
|
|
267
|
+
@click="goToAgent(row.original.id)"
|
|
268
|
+
/>
|
|
200
269
|
</template>
|
|
201
270
|
</UTable>
|
|
202
271
|
|
|
@@ -1,14 +1,153 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
|
-
|
|
2
|
+
// PR70 v2.87.0 — Health page polish.
|
|
3
|
+
// - 30s auto-refresh (paused while tab is hidden) + manual refresh
|
|
4
|
+
// - Last-checked timestamp in header
|
|
5
|
+
// - Severity-aware rendering (fail = red, warn = yellow)
|
|
6
|
+
// - Copy-fix button when a check has a fix command
|
|
7
|
+
// - Healthy banner ignores warnings (only blocking failures matter)
|
|
8
|
+
|
|
9
|
+
interface HealthCheck {
|
|
10
|
+
name: string
|
|
11
|
+
passed: boolean
|
|
12
|
+
fix: string
|
|
13
|
+
severity: 'fail' | 'warn'
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
interface HealthPayload {
|
|
17
|
+
checks: HealthCheck[]
|
|
18
|
+
passed: number
|
|
19
|
+
total: number
|
|
20
|
+
failed_blocking: number
|
|
21
|
+
warning_count: number
|
|
22
|
+
healthy: boolean
|
|
23
|
+
ts: string
|
|
24
|
+
}
|
|
3
25
|
|
|
4
26
|
const { fetchApi } = useApi()
|
|
27
|
+
const toast = useToast()
|
|
28
|
+
|
|
29
|
+
const {
|
|
30
|
+
data,
|
|
31
|
+
status,
|
|
32
|
+
error,
|
|
33
|
+
refresh,
|
|
34
|
+
} = await fetchApi<HealthPayload>('/api/health')
|
|
35
|
+
|
|
36
|
+
// ─── Auto-refresh ───────────────────────────────────────────────────────
|
|
37
|
+
|
|
38
|
+
let pollTimer: ReturnType<typeof setInterval> | null = null
|
|
39
|
+
|
|
40
|
+
function startPolling() {
|
|
41
|
+
stopPolling()
|
|
42
|
+
pollTimer = setInterval(() => {
|
|
43
|
+
refresh()
|
|
44
|
+
}, 30_000)
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function stopPolling() {
|
|
48
|
+
if (pollTimer !== null) {
|
|
49
|
+
clearInterval(pollTimer)
|
|
50
|
+
pollTimer = null
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function handleVisibility() {
|
|
55
|
+
if (typeof document === 'undefined') return
|
|
56
|
+
if (document.hidden) {
|
|
57
|
+
stopPolling()
|
|
58
|
+
} else {
|
|
59
|
+
refresh()
|
|
60
|
+
startPolling()
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
onMounted(() => {
|
|
65
|
+
if (typeof document !== 'undefined') {
|
|
66
|
+
document.addEventListener('visibilitychange', handleVisibility)
|
|
67
|
+
}
|
|
68
|
+
startPolling()
|
|
69
|
+
})
|
|
70
|
+
|
|
71
|
+
onBeforeUnmount(() => {
|
|
72
|
+
stopPolling()
|
|
73
|
+
if (typeof document !== 'undefined') {
|
|
74
|
+
document.removeEventListener('visibilitychange', handleVisibility)
|
|
75
|
+
}
|
|
76
|
+
})
|
|
77
|
+
|
|
78
|
+
// ─── Copy fix ───────────────────────────────────────────────────────────
|
|
79
|
+
|
|
80
|
+
const copied = ref<string | null>(null)
|
|
81
|
+
let copyTimer: ReturnType<typeof setTimeout> | null = null
|
|
82
|
+
|
|
83
|
+
async function copyFix(check: HealthCheck) {
|
|
84
|
+
if (!check.fix) return
|
|
85
|
+
if (typeof navigator === 'undefined' || !navigator.clipboard) {
|
|
86
|
+
toast.add({ title: 'Clipboard unavailable', color: 'warning' })
|
|
87
|
+
return
|
|
88
|
+
}
|
|
89
|
+
try {
|
|
90
|
+
await navigator.clipboard.writeText(check.fix)
|
|
91
|
+
copied.value = check.name
|
|
92
|
+
if (copyTimer) clearTimeout(copyTimer)
|
|
93
|
+
copyTimer = setTimeout(() => { copied.value = null; copyTimer = null }, 1500)
|
|
94
|
+
toast.add({
|
|
95
|
+
title: 'Fix copied',
|
|
96
|
+
description: check.fix,
|
|
97
|
+
color: 'success',
|
|
98
|
+
})
|
|
99
|
+
} catch (err) {
|
|
100
|
+
toast.add({
|
|
101
|
+
title: 'Copy failed',
|
|
102
|
+
description: err instanceof Error ? err.message : 'unknown error',
|
|
103
|
+
color: 'error',
|
|
104
|
+
})
|
|
105
|
+
}
|
|
106
|
+
}
|
|
5
107
|
|
|
6
|
-
|
|
108
|
+
onBeforeUnmount(() => {
|
|
109
|
+
if (copyTimer) clearTimeout(copyTimer)
|
|
110
|
+
})
|
|
7
111
|
|
|
8
|
-
|
|
112
|
+
// ─── Format helpers ─────────────────────────────────────────────────────
|
|
113
|
+
|
|
114
|
+
function formatTs(iso: string | undefined): string {
|
|
115
|
+
if (!iso) return ''
|
|
116
|
+
try {
|
|
117
|
+
return new Intl.DateTimeFormat('en-US', {
|
|
118
|
+
hour: '2-digit', minute: '2-digit', second: '2-digit',
|
|
119
|
+
}).format(new Date(iso))
|
|
120
|
+
} catch {
|
|
121
|
+
return iso
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
type CheckStatus = 'pass' | 'warn' | 'fail'
|
|
126
|
+
|
|
127
|
+
function statusOf(c: HealthCheck): CheckStatus {
|
|
128
|
+
if (c.passed) return 'pass'
|
|
129
|
+
return c.severity === 'warn' ? 'warn' : 'fail'
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
const STATUS_META: Record<CheckStatus, { icon: string; color: string; label: string }> = {
|
|
133
|
+
pass: { icon: 'i-lucide-check-circle', color: 'text-green-500', label: 'Pass' },
|
|
134
|
+
warn: { icon: 'i-lucide-alert-circle', color: 'text-yellow-500', label: 'Warn' },
|
|
135
|
+
fail: { icon: 'i-lucide-x-circle', color: 'text-red-500', label: 'Fail' },
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function statusBadgeColor(s: CheckStatus): 'success' | 'warning' | 'error' {
|
|
139
|
+
return s === 'pass' ? 'success' : s === 'warn' ? 'warning' : 'error'
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// ─── Aggregate display ──────────────────────────────────────────────────
|
|
143
|
+
|
|
144
|
+
const checks = computed<HealthCheck[]>(() => data.value?.checks ?? [])
|
|
9
145
|
const passed = computed(() => data.value?.passed ?? 0)
|
|
10
146
|
const total = computed(() => data.value?.total ?? 0)
|
|
11
|
-
const
|
|
147
|
+
const failedBlocking = computed(() => data.value?.failed_blocking ?? 0)
|
|
148
|
+
const warningCount = computed(() => data.value?.warning_count ?? 0)
|
|
149
|
+
const allPassed = computed(() => failedBlocking.value === 0 && warningCount.value === 0 && total.value > 0)
|
|
150
|
+
const someWarnings = computed(() => failedBlocking.value === 0 && warningCount.value > 0)
|
|
12
151
|
</script>
|
|
13
152
|
|
|
14
153
|
<template>
|
|
@@ -19,11 +158,28 @@ const allPassed = computed(() => passed.value === total.value && total.value > 0
|
|
|
19
158
|
<UDashboardSidebarCollapse />
|
|
20
159
|
</template>
|
|
21
160
|
<template #trailing>
|
|
161
|
+
<span
|
|
162
|
+
v-if="data?.ts"
|
|
163
|
+
class="text-xs text-muted"
|
|
164
|
+
:title="data.ts"
|
|
165
|
+
>
|
|
166
|
+
Last checked {{ formatTs(data.ts) }}
|
|
167
|
+
</span>
|
|
22
168
|
<UBadge
|
|
23
169
|
v-if="data"
|
|
24
170
|
:label="`${passed}/${total}`"
|
|
25
|
-
:color="allPassed ? 'success' : 'warning'"
|
|
171
|
+
:color="allPassed ? 'success' : someWarnings ? 'warning' : 'error'"
|
|
26
172
|
variant="subtle"
|
|
173
|
+
class="ml-3"
|
|
174
|
+
/>
|
|
175
|
+
</template>
|
|
176
|
+
<template #right>
|
|
177
|
+
<UButton
|
|
178
|
+
label="Refresh"
|
|
179
|
+
variant="ghost"
|
|
180
|
+
icon="i-lucide-refresh-cw"
|
|
181
|
+
size="sm"
|
|
182
|
+
@click="refresh()"
|
|
27
183
|
/>
|
|
28
184
|
</template>
|
|
29
185
|
</UDashboardNavbar>
|
|
@@ -39,45 +195,81 @@ const allPassed = computed(() => passed.value === total.value && total.value > 0
|
|
|
39
195
|
loading-label="Loading health checks"
|
|
40
196
|
:on-retry="() => refresh()"
|
|
41
197
|
>
|
|
42
|
-
<!-- Overall
|
|
198
|
+
<!-- Overall banner -->
|
|
43
199
|
<div
|
|
44
200
|
class="mb-6 rounded-lg border p-6 text-center"
|
|
45
|
-
:class="allPassed
|
|
201
|
+
:class="allPassed
|
|
202
|
+
? 'border-green-500/30 bg-green-500/5'
|
|
203
|
+
: someWarnings
|
|
204
|
+
? 'border-yellow-500/30 bg-yellow-500/5'
|
|
205
|
+
: 'border-red-500/30 bg-red-500/5'"
|
|
46
206
|
>
|
|
47
207
|
<UIcon
|
|
48
|
-
:name="allPassed
|
|
49
|
-
|
|
208
|
+
:name="allPassed
|
|
209
|
+
? 'i-lucide-check-circle'
|
|
210
|
+
: someWarnings
|
|
211
|
+
? 'i-lucide-alert-circle'
|
|
212
|
+
: 'i-lucide-x-circle'"
|
|
213
|
+
:class="allPassed
|
|
214
|
+
? 'text-green-500'
|
|
215
|
+
: someWarnings ? 'text-yellow-500' : 'text-red-500'"
|
|
50
216
|
class="size-12"
|
|
51
217
|
/>
|
|
52
218
|
<p class="mt-2 text-lg font-semibold text-highlighted">
|
|
53
|
-
|
|
219
|
+
<template v-if="allPassed">All Checks Passing</template>
|
|
220
|
+
<template v-else-if="someWarnings">
|
|
221
|
+
{{ warningCount }} Warning{{ warningCount === 1 ? '' : 's' }}
|
|
222
|
+
</template>
|
|
223
|
+
<template v-else>
|
|
224
|
+
{{ failedBlocking }} Blocking Failure{{ failedBlocking === 1 ? '' : 's' }}
|
|
225
|
+
</template>
|
|
226
|
+
</p>
|
|
227
|
+
<p class="text-sm text-muted">
|
|
228
|
+
{{ passed }} of {{ total }} checks passed
|
|
229
|
+
<template v-if="warningCount && failedBlocking">
|
|
230
|
+
· {{ warningCount }} warn · {{ failedBlocking }} blocking
|
|
231
|
+
</template>
|
|
54
232
|
</p>
|
|
55
|
-
<p class="text-sm text-muted">{{ passed }} of {{ total }} checks passed</p>
|
|
56
233
|
</div>
|
|
57
234
|
|
|
58
|
-
<!--
|
|
235
|
+
<!-- Check list -->
|
|
59
236
|
<div class="space-y-3">
|
|
60
237
|
<div
|
|
61
238
|
v-for="check in checks"
|
|
62
239
|
:key="check.name"
|
|
63
|
-
class="flex items-start gap-3 rounded-lg border
|
|
240
|
+
class="flex items-start gap-3 rounded-lg border p-4"
|
|
241
|
+
:class="{
|
|
242
|
+
'border-default': check.passed,
|
|
243
|
+
'border-yellow-500/30 bg-yellow-500/5': !check.passed && check.severity === 'warn',
|
|
244
|
+
'border-red-500/30 bg-red-500/5': !check.passed && check.severity === 'fail',
|
|
245
|
+
}"
|
|
64
246
|
>
|
|
65
247
|
<UIcon
|
|
66
|
-
:name="check.
|
|
67
|
-
:class="check.
|
|
248
|
+
:name="STATUS_META[statusOf(check)].icon"
|
|
249
|
+
:class="STATUS_META[statusOf(check)].color"
|
|
68
250
|
class="mt-0.5 size-5 shrink-0"
|
|
69
251
|
/>
|
|
70
|
-
<div class="flex-1">
|
|
252
|
+
<div class="flex-1 min-w-0">
|
|
71
253
|
<h4 class="font-medium text-highlighted">{{ check.name }}</h4>
|
|
72
254
|
<p v-if="!check.passed && check.fix" class="mt-1 text-sm text-muted">
|
|
73
|
-
Fix: {{ check.fix }}
|
|
255
|
+
Fix: <code class="font-mono text-xs">{{ check.fix }}</code>
|
|
74
256
|
</p>
|
|
75
257
|
</div>
|
|
258
|
+
<UButton
|
|
259
|
+
v-if="!check.passed && check.fix"
|
|
260
|
+
:icon="copied === check.name ? 'i-lucide-check' : 'i-lucide-copy'"
|
|
261
|
+
:color="copied === check.name ? 'success' : 'neutral'"
|
|
262
|
+
variant="ghost"
|
|
263
|
+
size="xs"
|
|
264
|
+
aria-label="Copy fix command"
|
|
265
|
+
@click="copyFix(check)"
|
|
266
|
+
/>
|
|
76
267
|
<UBadge
|
|
77
|
-
:label="check.
|
|
78
|
-
:color="check
|
|
268
|
+
:label="STATUS_META[statusOf(check)].label"
|
|
269
|
+
:color="statusBadgeColor(statusOf(check))"
|
|
79
270
|
variant="subtle"
|
|
80
271
|
size="sm"
|
|
272
|
+
class="shrink-0"
|
|
81
273
|
/>
|
|
82
274
|
</div>
|
|
83
275
|
</div>
|
|
@@ -175,6 +175,9 @@ export interface HealthCheck {
|
|
|
175
175
|
name: string
|
|
176
176
|
passed: boolean
|
|
177
177
|
fix: string
|
|
178
|
+
// PR70 v2.87.0 — backend now tags every check with a severity.
|
|
179
|
+
// 'fail' is must-pass; 'warn' is recommended but non-blocking.
|
|
180
|
+
severity?: 'fail' | 'warn'
|
|
178
181
|
}
|
|
179
182
|
|
|
180
183
|
export interface Persona {
|
package/package.json
CHANGED
package/pyproject.toml
CHANGED
|
Binary file
|
package/scripts/dashboard-api.py
CHANGED
|
@@ -173,6 +173,51 @@ def agents(dept: Optional[str] = Query(None)):
|
|
|
173
173
|
return {"agents": data, "total": len(data)}
|
|
174
174
|
|
|
175
175
|
|
|
176
|
+
@app.get("/api/agents/activity")
|
|
177
|
+
def agents_activity(period: str = "week"):
|
|
178
|
+
"""Per-department activity from the PR47 LLM cost telemetry.
|
|
179
|
+
|
|
180
|
+
Returns ``{by_department: {dev: {call_count, total_cost_usd,
|
|
181
|
+
total_tokens_in, total_tokens_out}}}`` derived from rows whose
|
|
182
|
+
``category`` field starts with ``subagent:``. Each agent's
|
|
183
|
+
dispatch is currently tagged at the department level — finer
|
|
184
|
+
per-agent attribution will land when orchestrators set
|
|
185
|
+
``ARKA_CALL_CATEGORY=subagent:<dept>:<agent>``.
|
|
186
|
+
"""
|
|
187
|
+
try:
|
|
188
|
+
from core.runtime.llm_cost_telemetry import summarise, VALID_PERIODS
|
|
189
|
+
except Exception: # pragma: no cover - import guard
|
|
190
|
+
return {"by_department": {}, "period": period}
|
|
191
|
+
if period not in VALID_PERIODS:
|
|
192
|
+
period = "week"
|
|
193
|
+
summary = summarise(period=period)
|
|
194
|
+
out: dict[str, dict] = {}
|
|
195
|
+
for category, row in (summary.by_category or {}).items():
|
|
196
|
+
if not isinstance(category, str) or not category.startswith("subagent:"):
|
|
197
|
+
continue
|
|
198
|
+
dept = category.split(":", 1)[1] or "unknown"
|
|
199
|
+
bucket = out.setdefault(dept, {
|
|
200
|
+
"call_count": 0,
|
|
201
|
+
"total_cost_usd": 0.0,
|
|
202
|
+
"any_cost_known": False,
|
|
203
|
+
"total_tokens_in": 0,
|
|
204
|
+
"total_tokens_out": 0,
|
|
205
|
+
})
|
|
206
|
+
bucket["call_count"] += row.get("call_count", 0)
|
|
207
|
+
bucket["total_tokens_in"] += row.get("total_tokens_in", 0)
|
|
208
|
+
bucket["total_tokens_out"] += row.get("total_tokens_out", 0)
|
|
209
|
+
cost = row.get("total_cost_usd")
|
|
210
|
+
if isinstance(cost, (int, float)):
|
|
211
|
+
bucket["total_cost_usd"] += float(cost)
|
|
212
|
+
bucket["any_cost_known"] = True
|
|
213
|
+
for dept, b in out.items():
|
|
214
|
+
if not b.pop("any_cost_known"):
|
|
215
|
+
b["total_cost_usd"] = None
|
|
216
|
+
else:
|
|
217
|
+
b["total_cost_usd"] = round(b["total_cost_usd"], 6)
|
|
218
|
+
return {"by_department": out, "period": period}
|
|
219
|
+
|
|
220
|
+
|
|
176
221
|
@app.get("/api/agents/{agent_id}")
|
|
177
222
|
def agent_detail(agent_id: str):
|
|
178
223
|
"""Get full agent detail including YAML data."""
|
|
@@ -569,18 +614,38 @@ def knowledge_search(q: str = Query(...), top_k: int = Query(5)):
|
|
|
569
614
|
|
|
570
615
|
@app.get("/api/health")
|
|
571
616
|
def health():
|
|
572
|
-
|
|
617
|
+
"""PR70 v2.87.0 — per-check severity + response timestamp.
|
|
618
|
+
|
|
619
|
+
Each check now carries a `severity` field:
|
|
620
|
+
- "fail" — must-pass; missing breaks ArkaOS
|
|
621
|
+
- "warn" — recommended; missing means a degraded but workable env
|
|
622
|
+
|
|
623
|
+
Response also carries `ts` so the UI can show "last checked".
|
|
624
|
+
Frontend polls every 30s and surfaces copy-fix buttons.
|
|
625
|
+
"""
|
|
626
|
+
from datetime import datetime, timezone
|
|
627
|
+
|
|
628
|
+
checks: list[dict] = []
|
|
573
629
|
arkaos_home = Path.home() / ".arkaos"
|
|
574
630
|
|
|
575
|
-
def check(name, condition, fix=""):
|
|
576
|
-
checks.append({
|
|
631
|
+
def check(name: str, condition: bool, fix: str = "", severity: str = "fail"):
|
|
632
|
+
checks.append({
|
|
633
|
+
"name": name,
|
|
634
|
+
"passed": condition,
|
|
635
|
+
"fix": fix,
|
|
636
|
+
"severity": severity,
|
|
637
|
+
})
|
|
577
638
|
|
|
578
|
-
check("install_dir", arkaos_home.exists(), "
|
|
579
|
-
check("manifest", (arkaos_home / "install-manifest.json").exists(),
|
|
639
|
+
check("install_dir", arkaos_home.exists(), "npx arkaos install")
|
|
640
|
+
check("manifest", (arkaos_home / "install-manifest.json").exists(),
|
|
641
|
+
"npx arkaos install")
|
|
580
642
|
check("constitution", (ARKAOS_ROOT / "config" / "constitution.yaml").exists())
|
|
581
|
-
check("agents_registry",
|
|
582
|
-
|
|
583
|
-
check("
|
|
643
|
+
check("agents_registry",
|
|
644
|
+
(ARKAOS_ROOT / "knowledge" / "agents-registry-v2.json").exists())
|
|
645
|
+
check("commands_registry",
|
|
646
|
+
(ARKAOS_ROOT / "knowledge" / "commands-registry-v2.json").exists())
|
|
647
|
+
check("hooks_dir", (arkaos_home / "config" / "hooks").exists(),
|
|
648
|
+
"npx arkaos install")
|
|
584
649
|
|
|
585
650
|
try:
|
|
586
651
|
subprocess.run(["python3", "--version"], capture_output=True, timeout=2)
|
|
@@ -588,10 +653,34 @@ def health():
|
|
|
588
653
|
except Exception:
|
|
589
654
|
check("python", False, "Install Python 3.11+")
|
|
590
655
|
|
|
591
|
-
|
|
656
|
+
# Telemetry + knowledge — warn-only; missing them is a degraded
|
|
657
|
+
# but workable state (new installs, never-indexed-anything).
|
|
658
|
+
check("knowledge_db", (arkaos_home / "knowledge.db").exists(),
|
|
659
|
+
"Open the Knowledge tab and ingest a source",
|
|
660
|
+
severity="warn")
|
|
661
|
+
check("profile",
|
|
662
|
+
(arkaos_home / "profile.json").exists(),
|
|
663
|
+
"Open Settings → Profile to introduce yourself",
|
|
664
|
+
severity="warn")
|
|
592
665
|
|
|
593
666
|
passed = sum(1 for c in checks if c["passed"])
|
|
594
|
-
|
|
667
|
+
failed_blocking = sum(
|
|
668
|
+
1 for c in checks
|
|
669
|
+
if not c["passed"] and c["severity"] == "fail"
|
|
670
|
+
)
|
|
671
|
+
warning_count = sum(
|
|
672
|
+
1 for c in checks
|
|
673
|
+
if not c["passed"] and c["severity"] == "warn"
|
|
674
|
+
)
|
|
675
|
+
return {
|
|
676
|
+
"checks": checks,
|
|
677
|
+
"passed": passed,
|
|
678
|
+
"total": len(checks),
|
|
679
|
+
"failed_blocking": failed_blocking,
|
|
680
|
+
"warning_count": warning_count,
|
|
681
|
+
"healthy": failed_blocking == 0,
|
|
682
|
+
"ts": datetime.now(timezone.utc).isoformat(),
|
|
683
|
+
}
|
|
595
684
|
|
|
596
685
|
|
|
597
686
|
# --- Personas ---
|