arkaos 3.25.0 → 3.27.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/core/knowledge/__pycache__/vector_store.cpython-313.pyc +0 -0
- package/dashboard/app/layouts/default.vue +7 -0
- package/dashboard/app/pages/agents/[id].vue +66 -0
- package/dashboard/app/pages/departments/[dept].vue +158 -0
- package/dashboard/app/pages/departments/index.vue +127 -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 +211 -0
package/VERSION
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
3.
|
|
1
|
+
3.27.0
|
|
Binary file
|
|
@@ -20,6 +20,13 @@ const links = [[{
|
|
|
20
20
|
onSelect: () => {
|
|
21
21
|
open.value = false
|
|
22
22
|
}
|
|
23
|
+
}, {
|
|
24
|
+
label: 'Departments',
|
|
25
|
+
icon: 'i-lucide-folder-tree',
|
|
26
|
+
to: '/departments',
|
|
27
|
+
onSelect: () => {
|
|
28
|
+
open.value = false
|
|
29
|
+
}
|
|
23
30
|
}, {
|
|
24
31
|
label: 'Personas',
|
|
25
32
|
icon: 'i-lucide-user-plus',
|
|
@@ -28,6 +28,35 @@ const deptActivity = computed<ActivityRow | null>(() =>
|
|
|
28
28
|
(activityData.value?.by_department?.[agent.value?.department ?? ''] ?? null),
|
|
29
29
|
)
|
|
30
30
|
|
|
31
|
+
// PR88d v3.26.0 — agent history (git log + trash entries)
|
|
32
|
+
interface HistoryEvent {
|
|
33
|
+
kind: string
|
|
34
|
+
ts: string | null
|
|
35
|
+
summary: string
|
|
36
|
+
ref?: string
|
|
37
|
+
author?: string
|
|
38
|
+
}
|
|
39
|
+
const { data: historyData } = fetchApi<{ events: HistoryEvent[] }>(
|
|
40
|
+
`/api/agents/${agentId}/history?limit=20`,
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
const historyEvents = computed<HistoryEvent[]>(() => historyData.value?.events ?? [])
|
|
44
|
+
|
|
45
|
+
function historyKindIcon(kind: string): string {
|
|
46
|
+
return ({
|
|
47
|
+
'git-commit': 'i-lucide-git-commit',
|
|
48
|
+
'agent-delete': 'i-lucide-trash-2',
|
|
49
|
+
'agent-move': 'i-lucide-folder-tree',
|
|
50
|
+
} as Record<string, string>)[kind] ?? 'i-lucide-circle'
|
|
51
|
+
}
|
|
52
|
+
function historyKindColor(kind: string): string {
|
|
53
|
+
return ({
|
|
54
|
+
'git-commit': 'text-blue-500',
|
|
55
|
+
'agent-delete': 'text-red-500',
|
|
56
|
+
'agent-move': 'text-amber-500',
|
|
57
|
+
} as Record<string, string>)[kind] ?? 'text-muted'
|
|
58
|
+
}
|
|
59
|
+
|
|
31
60
|
// PR83d v3.6.0 + PR86b v3.16.0 — activity strip (30d, agent or dept scope)
|
|
32
61
|
interface ActivityStrip {
|
|
33
62
|
period: string
|
|
@@ -443,6 +472,43 @@ function formatTokens(n: number): string {
|
|
|
443
472
|
/>
|
|
444
473
|
</section>
|
|
445
474
|
|
|
475
|
+
<!-- ===== HISTORY TIMELINE (PR88d) ===== -->
|
|
476
|
+
<section
|
|
477
|
+
v-if="historyEvents.length > 0"
|
|
478
|
+
class="rounded-xl border border-default bg-elevated/10 p-5"
|
|
479
|
+
>
|
|
480
|
+
<h3 class="text-sm font-semibold uppercase tracking-wide text-muted mb-4">
|
|
481
|
+
History
|
|
482
|
+
</h3>
|
|
483
|
+
<ol class="relative border-l border-default ml-2 space-y-3">
|
|
484
|
+
<li
|
|
485
|
+
v-for="(ev, idx) in historyEvents"
|
|
486
|
+
:key="idx"
|
|
487
|
+
class="ml-4"
|
|
488
|
+
>
|
|
489
|
+
<span
|
|
490
|
+
class="absolute -left-1.5 size-3 rounded-full bg-elevated border border-default flex items-center justify-center"
|
|
491
|
+
>
|
|
492
|
+
<UIcon :name="historyKindIcon(ev.kind)" :class="['size-2', historyKindColor(ev.kind)]" />
|
|
493
|
+
</span>
|
|
494
|
+
<div class="rounded-lg border border-default p-3 bg-elevated/20">
|
|
495
|
+
<div class="flex items-center gap-2 flex-wrap text-xs">
|
|
496
|
+
<span class="font-mono text-muted">{{ ev.ts ? formatRelative(ev.ts) : '—' }}</span>
|
|
497
|
+
<UBadge
|
|
498
|
+
:label="ev.kind"
|
|
499
|
+
:color="ev.kind === 'git-commit' ? 'primary' : ev.kind === 'agent-move' ? 'warning' : 'error'"
|
|
500
|
+
variant="subtle"
|
|
501
|
+
size="xs"
|
|
502
|
+
/>
|
|
503
|
+
<code v-if="ev.ref" class="font-mono text-muted">{{ ev.ref }}</code>
|
|
504
|
+
<span v-if="ev.author" class="text-muted">· {{ ev.author }}</span>
|
|
505
|
+
</div>
|
|
506
|
+
<p class="text-sm mt-1">{{ ev.summary }}</p>
|
|
507
|
+
</div>
|
|
508
|
+
</li>
|
|
509
|
+
</ol>
|
|
510
|
+
</section>
|
|
511
|
+
|
|
446
512
|
<AgentEditDrawer
|
|
447
513
|
v-model="editOpen"
|
|
448
514
|
:agent="agent"
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
// PR89a v3.27.0 — Department detail page.
|
|
3
|
+
//
|
|
4
|
+
// Shows agents in the department + its workflows + 30d cost.
|
|
5
|
+
|
|
6
|
+
const route = useRoute()
|
|
7
|
+
const deptId = computed(() => String(route.params.dept ?? ''))
|
|
8
|
+
const { fetchApi } = useApi()
|
|
9
|
+
|
|
10
|
+
interface AgentLite {
|
|
11
|
+
id: string
|
|
12
|
+
name?: string
|
|
13
|
+
role?: string
|
|
14
|
+
tier?: number
|
|
15
|
+
mbti?: string
|
|
16
|
+
disc?: { primary?: string, secondary?: string }
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
interface WorkflowLite {
|
|
20
|
+
id: string
|
|
21
|
+
name: string
|
|
22
|
+
tier: string
|
|
23
|
+
command: string
|
|
24
|
+
phases_count: number
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
interface DeptDetail {
|
|
28
|
+
department: string
|
|
29
|
+
agents: AgentLite[]
|
|
30
|
+
workflows: WorkflowLite[]
|
|
31
|
+
calls_30d: number
|
|
32
|
+
cost_usd_30d: number | null
|
|
33
|
+
error?: string
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const { data, status, error, refresh } = await fetchApi<DeptDetail>(
|
|
37
|
+
`/api/departments/${deptId.value}`,
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
const errorMsg = computed(() => data.value?.error || error.value?.message || null)
|
|
41
|
+
const detail = computed<DeptDetail | null>(() => {
|
|
42
|
+
if (!data.value || data.value.error) return null
|
|
43
|
+
return data.value
|
|
44
|
+
})
|
|
45
|
+
|
|
46
|
+
function formatCost(cost: number | null): string {
|
|
47
|
+
if (cost === null || cost === undefined) return '—'
|
|
48
|
+
if (cost < 0.01) return '<$0.01'
|
|
49
|
+
if (cost < 1) return `$${cost.toFixed(3)}`
|
|
50
|
+
return `$${cost.toFixed(2)}`
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const tierColor = (tier: number | undefined) => {
|
|
54
|
+
const colors: Record<number, 'error' | 'warning' | 'primary' | 'neutral'> = {
|
|
55
|
+
0: 'error', 1: 'warning', 2: 'primary', 3: 'neutral',
|
|
56
|
+
}
|
|
57
|
+
return colors[tier ?? 99] ?? 'neutral'
|
|
58
|
+
}
|
|
59
|
+
</script>
|
|
60
|
+
|
|
61
|
+
<template>
|
|
62
|
+
<UDashboardPanel :id="`dept-${deptId}`">
|
|
63
|
+
<template #header>
|
|
64
|
+
<UDashboardNavbar :title="`Department · ${deptId}`">
|
|
65
|
+
<template #leading>
|
|
66
|
+
<UButton icon="i-lucide-arrow-left" variant="ghost" size="sm" to="/departments" aria-label="Back" />
|
|
67
|
+
</template>
|
|
68
|
+
</UDashboardNavbar>
|
|
69
|
+
</template>
|
|
70
|
+
|
|
71
|
+
<template #body>
|
|
72
|
+
<DashboardState
|
|
73
|
+
:status="status"
|
|
74
|
+
:error="errorMsg ? new Error(errorMsg) : null"
|
|
75
|
+
:empty="!detail"
|
|
76
|
+
empty-title="Department not found"
|
|
77
|
+
empty-icon="i-lucide-folder-x"
|
|
78
|
+
loading-label="Loading department"
|
|
79
|
+
:on-retry="() => refresh()"
|
|
80
|
+
>
|
|
81
|
+
<div v-if="detail" class="space-y-5 max-w-5xl">
|
|
82
|
+
<!-- Stats row -->
|
|
83
|
+
<section class="grid grid-cols-2 md:grid-cols-4 gap-3">
|
|
84
|
+
<div class="rounded-xl border border-default p-4 bg-elevated/20">
|
|
85
|
+
<p class="text-sm font-semibold text-muted uppercase tracking-wide mb-1">Agents</p>
|
|
86
|
+
<p class="text-2xl font-bold">{{ detail.agents.length }}</p>
|
|
87
|
+
</div>
|
|
88
|
+
<div class="rounded-xl border border-default p-4 bg-elevated/20">
|
|
89
|
+
<p class="text-sm font-semibold text-muted uppercase tracking-wide mb-1">Workflows</p>
|
|
90
|
+
<p class="text-2xl font-bold">{{ detail.workflows.length }}</p>
|
|
91
|
+
</div>
|
|
92
|
+
<div class="rounded-xl border border-default p-4 bg-elevated/20">
|
|
93
|
+
<p class="text-sm font-semibold text-muted uppercase tracking-wide mb-1">Calls (30d)</p>
|
|
94
|
+
<p class="text-2xl font-bold">{{ detail.calls_30d }}</p>
|
|
95
|
+
</div>
|
|
96
|
+
<div class="rounded-xl border border-default p-4 bg-elevated/20">
|
|
97
|
+
<p class="text-sm font-semibold text-muted uppercase tracking-wide mb-1">Cost (30d)</p>
|
|
98
|
+
<p class="text-2xl font-bold">{{ formatCost(detail.cost_usd_30d) }}</p>
|
|
99
|
+
</div>
|
|
100
|
+
</section>
|
|
101
|
+
|
|
102
|
+
<!-- Agents -->
|
|
103
|
+
<section>
|
|
104
|
+
<h2 class="text-sm font-semibold uppercase tracking-wide text-muted mb-3">Agents</h2>
|
|
105
|
+
<div class="grid grid-cols-1 md:grid-cols-2 gap-2">
|
|
106
|
+
<NuxtLink
|
|
107
|
+
v-for="a in detail.agents"
|
|
108
|
+
:key="a.id"
|
|
109
|
+
:to="`/agents/${a.id}`"
|
|
110
|
+
class="block rounded-lg border border-default p-3 hover:border-primary/40 transition-colors"
|
|
111
|
+
>
|
|
112
|
+
<div class="flex items-center justify-between gap-3">
|
|
113
|
+
<div class="min-w-0">
|
|
114
|
+
<p class="font-semibold truncate">{{ a.name }}</p>
|
|
115
|
+
<p class="text-xs text-muted truncate">{{ a.role || '—' }}</p>
|
|
116
|
+
</div>
|
|
117
|
+
<div class="flex items-center gap-2 shrink-0">
|
|
118
|
+
<UBadge
|
|
119
|
+
:label="`T${a.tier}`"
|
|
120
|
+
:color="tierColor(a.tier)"
|
|
121
|
+
variant="subtle"
|
|
122
|
+
size="xs"
|
|
123
|
+
/>
|
|
124
|
+
<UBadge v-if="a.mbti" :label="a.mbti" variant="soft" size="xs" />
|
|
125
|
+
</div>
|
|
126
|
+
</div>
|
|
127
|
+
</NuxtLink>
|
|
128
|
+
</div>
|
|
129
|
+
</section>
|
|
130
|
+
|
|
131
|
+
<!-- Workflows -->
|
|
132
|
+
<section v-if="detail.workflows.length > 0">
|
|
133
|
+
<h2 class="text-sm font-semibold uppercase tracking-wide text-muted mb-3">Workflows</h2>
|
|
134
|
+
<div class="space-y-2">
|
|
135
|
+
<NuxtLink
|
|
136
|
+
v-for="w in detail.workflows"
|
|
137
|
+
:key="w.id"
|
|
138
|
+
to="/workflows"
|
|
139
|
+
class="block rounded-lg border border-default p-3 hover:border-primary/40 transition-colors"
|
|
140
|
+
>
|
|
141
|
+
<div class="flex items-center justify-between gap-3">
|
|
142
|
+
<div class="min-w-0">
|
|
143
|
+
<p class="font-semibold truncate">{{ w.name }}</p>
|
|
144
|
+
<p class="text-xs text-muted font-mono truncate">{{ w.command || w.id }}</p>
|
|
145
|
+
</div>
|
|
146
|
+
<div class="flex items-center gap-2 shrink-0 text-xs">
|
|
147
|
+
<UBadge v-if="w.tier" :label="w.tier" variant="subtle" size="xs" />
|
|
148
|
+
<span class="text-muted">{{ w.phases_count }} phase{{ w.phases_count === 1 ? '' : 's' }}</span>
|
|
149
|
+
</div>
|
|
150
|
+
</div>
|
|
151
|
+
</NuxtLink>
|
|
152
|
+
</div>
|
|
153
|
+
</section>
|
|
154
|
+
</div>
|
|
155
|
+
</DashboardState>
|
|
156
|
+
</template>
|
|
157
|
+
</UDashboardPanel>
|
|
158
|
+
</template>
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
// PR89a v3.27.0 — Departments index.
|
|
3
|
+
//
|
|
4
|
+
// Lists every department with agent count, tier distribution, and 30d
|
|
5
|
+
// cost. Click a row to drill into /departments/{dept}.
|
|
6
|
+
|
|
7
|
+
import type { TableColumn } from '@nuxt/ui'
|
|
8
|
+
|
|
9
|
+
interface DeptRow {
|
|
10
|
+
department: string
|
|
11
|
+
agent_count: number
|
|
12
|
+
tier_counts: Record<'0' | '1' | '2' | '3', number>
|
|
13
|
+
calls_30d: number
|
|
14
|
+
cost_usd_30d: number | null
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const { fetchApi } = useApi()
|
|
18
|
+
const { data, status, error, refresh } = await fetchApi<{
|
|
19
|
+
departments: DeptRow[]
|
|
20
|
+
total: number
|
|
21
|
+
}>('/api/departments')
|
|
22
|
+
|
|
23
|
+
const rows = computed<DeptRow[]>(() => data.value?.departments ?? [])
|
|
24
|
+
const search = ref('')
|
|
25
|
+
const filtered = computed(() => {
|
|
26
|
+
const q = search.value.toLowerCase().trim()
|
|
27
|
+
if (!q) return rows.value
|
|
28
|
+
return rows.value.filter((r) => r.department.toLowerCase().includes(q))
|
|
29
|
+
})
|
|
30
|
+
|
|
31
|
+
const columns: TableColumn<DeptRow>[] = [
|
|
32
|
+
{ accessorKey: 'department', header: 'Department' },
|
|
33
|
+
{ accessorKey: 'agent_count', header: 'Agents' },
|
|
34
|
+
{ id: 'tiers', header: 'Tiers (0/1/2/3)' },
|
|
35
|
+
{ accessorKey: 'calls_30d', header: 'Calls (30d)' },
|
|
36
|
+
{ accessorKey: 'cost_usd_30d', header: 'Cost (30d)' },
|
|
37
|
+
{ id: 'actions', header: '' },
|
|
38
|
+
]
|
|
39
|
+
|
|
40
|
+
function formatCost(cost: number | null): string {
|
|
41
|
+
if (cost === null || cost === undefined) return '—'
|
|
42
|
+
if (cost < 0.01) return '<$0.01'
|
|
43
|
+
if (cost < 1) return `$${cost.toFixed(3)}`
|
|
44
|
+
return `$${cost.toFixed(2)}`
|
|
45
|
+
}
|
|
46
|
+
</script>
|
|
47
|
+
|
|
48
|
+
<template>
|
|
49
|
+
<UDashboardPanel id="departments">
|
|
50
|
+
<template #header>
|
|
51
|
+
<UDashboardNavbar title="Departments">
|
|
52
|
+
<template #leading>
|
|
53
|
+
<UDashboardSidebarCollapse />
|
|
54
|
+
</template>
|
|
55
|
+
<template #trailing>
|
|
56
|
+
<UBadge v-if="data?.total" :label="String(data.total)" variant="subtle" />
|
|
57
|
+
</template>
|
|
58
|
+
</UDashboardNavbar>
|
|
59
|
+
</template>
|
|
60
|
+
|
|
61
|
+
<template #body>
|
|
62
|
+
<DashboardState
|
|
63
|
+
:status="status"
|
|
64
|
+
:error="error"
|
|
65
|
+
:empty="!rows.length"
|
|
66
|
+
empty-title="No departments yet"
|
|
67
|
+
empty-icon="i-lucide-folder-tree"
|
|
68
|
+
loading-label="Loading departments"
|
|
69
|
+
:on-retry="() => refresh()"
|
|
70
|
+
>
|
|
71
|
+
<div class="flex items-center gap-3 mb-4">
|
|
72
|
+
<UInput
|
|
73
|
+
v-model="search"
|
|
74
|
+
class="max-w-sm"
|
|
75
|
+
icon="i-lucide-search"
|
|
76
|
+
placeholder="Filter departments…"
|
|
77
|
+
/>
|
|
78
|
+
<span class="ml-auto text-xs text-muted">
|
|
79
|
+
{{ filtered.length }} dept{{ filtered.length === 1 ? '' : 's' }}
|
|
80
|
+
</span>
|
|
81
|
+
</div>
|
|
82
|
+
|
|
83
|
+
<UTable
|
|
84
|
+
:data="filtered"
|
|
85
|
+
:columns="columns"
|
|
86
|
+
:loading="status === 'pending'"
|
|
87
|
+
:ui="{
|
|
88
|
+
tbody: '[&>tr]:cursor-pointer [&>tr]:hover:bg-elevated/50 [&>tr]:transition-colors',
|
|
89
|
+
th: 'py-2 first:rounded-l-lg last:rounded-r-lg border-y border-default first:border-l last:border-r',
|
|
90
|
+
td: 'border-b border-default',
|
|
91
|
+
}"
|
|
92
|
+
@select="(row: { original: DeptRow }) => navigateTo(`/departments/${row.original.department}`)"
|
|
93
|
+
>
|
|
94
|
+
<template #department-cell="{ row }">
|
|
95
|
+
<span class="font-semibold capitalize">{{ row.original.department }}</span>
|
|
96
|
+
</template>
|
|
97
|
+
<template #agent_count-cell="{ row }">
|
|
98
|
+
<span class="font-mono font-semibold">{{ row.original.agent_count }}</span>
|
|
99
|
+
</template>
|
|
100
|
+
<template #tiers-cell="{ row }">
|
|
101
|
+
<div class="flex items-center gap-1 text-xs font-mono">
|
|
102
|
+
<UBadge v-if="row.original.tier_counts['0'] > 0" :label="`0:${row.original.tier_counts['0']}`" color="error" variant="subtle" size="xs" />
|
|
103
|
+
<UBadge v-if="row.original.tier_counts['1'] > 0" :label="`1:${row.original.tier_counts['1']}`" color="warning" variant="subtle" size="xs" />
|
|
104
|
+
<UBadge v-if="row.original.tier_counts['2'] > 0" :label="`2:${row.original.tier_counts['2']}`" color="primary" variant="subtle" size="xs" />
|
|
105
|
+
<UBadge v-if="row.original.tier_counts['3'] > 0" :label="`3:${row.original.tier_counts['3']}`" color="neutral" variant="subtle" size="xs" />
|
|
106
|
+
</div>
|
|
107
|
+
</template>
|
|
108
|
+
<template #calls_30d-cell="{ row }">
|
|
109
|
+
<span class="font-mono text-sm">{{ row.original.calls_30d }}</span>
|
|
110
|
+
</template>
|
|
111
|
+
<template #cost_usd_30d-cell="{ row }">
|
|
112
|
+
<span class="font-mono text-sm font-semibold">{{ formatCost(row.original.cost_usd_30d) }}</span>
|
|
113
|
+
</template>
|
|
114
|
+
<template #actions-cell="{ row }">
|
|
115
|
+
<UButton
|
|
116
|
+
icon="i-lucide-arrow-right"
|
|
117
|
+
variant="ghost"
|
|
118
|
+
size="xs"
|
|
119
|
+
aria-label="Open department"
|
|
120
|
+
@click.stop="navigateTo(`/departments/${row.original.department}`)"
|
|
121
|
+
/>
|
|
122
|
+
</template>
|
|
123
|
+
</UTable>
|
|
124
|
+
</DashboardState>
|
|
125
|
+
</template>
|
|
126
|
+
</UDashboardPanel>
|
|
127
|
+
</template>
|
package/package.json
CHANGED
package/pyproject.toml
CHANGED
|
Binary file
|
package/scripts/dashboard-api.py
CHANGED
|
@@ -322,6 +322,96 @@ def agent_activity_strip(agent_id: str, period: str = "month"):
|
|
|
322
322
|
}
|
|
323
323
|
|
|
324
324
|
|
|
325
|
+
@app.get("/api/agents/{agent_id}/history")
|
|
326
|
+
def agent_history(agent_id: str, limit: int = 20):
|
|
327
|
+
"""PR88d v3.26.0 — combined history for an agent.
|
|
328
|
+
|
|
329
|
+
Sources:
|
|
330
|
+
- git log of the YAML file (commit hash, date, subject, author)
|
|
331
|
+
- trash entries (agent-delete + agent-move) where ``item_id``
|
|
332
|
+
matches
|
|
333
|
+
|
|
334
|
+
Returns ``{events: [{kind, ts, summary, ref?, author?}]}`` sorted
|
|
335
|
+
desc by ts.
|
|
336
|
+
"""
|
|
337
|
+
events: list[dict] = []
|
|
338
|
+
yaml_file = _resolve_agent_yaml(agent_id)
|
|
339
|
+
if yaml_file is not None:
|
|
340
|
+
events.extend(_agent_git_log(yaml_file, limit=limit))
|
|
341
|
+
try:
|
|
342
|
+
from core import trash as _trash
|
|
343
|
+
for entry in _trash.list_trash(limit=50):
|
|
344
|
+
if entry.get("item_id") == agent_id and str(entry.get("kind", "")).startswith("agent-"):
|
|
345
|
+
events.append({
|
|
346
|
+
"kind": entry.get("kind"),
|
|
347
|
+
"ts": _trash_ts_to_iso(entry.get("timestamp")),
|
|
348
|
+
"summary": _trash_summary(entry),
|
|
349
|
+
"ref": entry.get("id"),
|
|
350
|
+
})
|
|
351
|
+
except Exception: # noqa: BLE001
|
|
352
|
+
pass
|
|
353
|
+
events.sort(key=lambda e: str(e.get("ts") or ""), reverse=True)
|
|
354
|
+
return {"events": events[: max(0, int(limit))]}
|
|
355
|
+
|
|
356
|
+
|
|
357
|
+
def _agent_git_log(yaml_file: Path, limit: int = 20) -> list[dict]:
|
|
358
|
+
"""Run ``git log`` on the YAML file. Best-effort — empty on error."""
|
|
359
|
+
try:
|
|
360
|
+
rel = yaml_file.relative_to(ARKAOS_ROOT).as_posix()
|
|
361
|
+
except ValueError:
|
|
362
|
+
return []
|
|
363
|
+
try:
|
|
364
|
+
result = subprocess.run(
|
|
365
|
+
[
|
|
366
|
+
"git", "log", "--follow", f"-n{int(limit)}",
|
|
367
|
+
"--pretty=format:%H%x09%cI%x09%an%x09%s",
|
|
368
|
+
"--", rel,
|
|
369
|
+
],
|
|
370
|
+
cwd=str(ARKAOS_ROOT),
|
|
371
|
+
capture_output=True, text=True, timeout=5,
|
|
372
|
+
)
|
|
373
|
+
except (subprocess.SubprocessError, OSError):
|
|
374
|
+
return []
|
|
375
|
+
if result.returncode != 0:
|
|
376
|
+
return []
|
|
377
|
+
rows: list[dict] = []
|
|
378
|
+
for line in result.stdout.strip().split("\n"):
|
|
379
|
+
if not line:
|
|
380
|
+
continue
|
|
381
|
+
parts = line.split("\t", 3)
|
|
382
|
+
if len(parts) < 4:
|
|
383
|
+
continue
|
|
384
|
+
sha, iso, author, subject = parts
|
|
385
|
+
rows.append({
|
|
386
|
+
"kind": "git-commit",
|
|
387
|
+
"ts": iso,
|
|
388
|
+
"summary": subject,
|
|
389
|
+
"ref": sha[:8],
|
|
390
|
+
"author": author,
|
|
391
|
+
})
|
|
392
|
+
return rows
|
|
393
|
+
|
|
394
|
+
|
|
395
|
+
def _trash_ts_to_iso(ts: object) -> str | None:
|
|
396
|
+
if ts is None:
|
|
397
|
+
return None
|
|
398
|
+
try:
|
|
399
|
+
from datetime import datetime, timezone
|
|
400
|
+
return datetime.fromtimestamp(float(ts), tz=timezone.utc).isoformat()
|
|
401
|
+
except (TypeError, ValueError, OSError):
|
|
402
|
+
return None
|
|
403
|
+
|
|
404
|
+
|
|
405
|
+
def _trash_summary(entry: dict) -> str:
|
|
406
|
+
kind = entry.get("kind") or ""
|
|
407
|
+
if kind == "agent-move":
|
|
408
|
+
new_path = entry.get("new_path") or ""
|
|
409
|
+
return f"Moved to {Path(new_path).parent.parent.name if new_path else '?'}"
|
|
410
|
+
if kind == "agent-delete":
|
|
411
|
+
return "Deleted (restorable from /trash)"
|
|
412
|
+
return kind
|
|
413
|
+
|
|
414
|
+
|
|
325
415
|
@app.get("/api/agents/{agent_id}/activity")
|
|
326
416
|
def agent_activity_detail(agent_id: str, period: str = "month"):
|
|
327
417
|
"""PR86b v3.16.0 — alias for /activity-strip. Same payload shape.
|
|
@@ -1404,6 +1494,127 @@ def agent_export_to_vault(agent_id: str):
|
|
|
1404
1494
|
return {"exported": True, "path": str(res.path), "vault_path": str(res.vault_path)}
|
|
1405
1495
|
|
|
1406
1496
|
|
|
1497
|
+
# --- Departments (PR89a v3.27.0) ---
|
|
1498
|
+
|
|
1499
|
+
@app.get("/api/departments")
|
|
1500
|
+
def departments_list():
|
|
1501
|
+
"""List every department with agent count + 30d cost summary."""
|
|
1502
|
+
agents = _load_agents()
|
|
1503
|
+
by_dept: dict[str, dict] = {}
|
|
1504
|
+
for a in agents:
|
|
1505
|
+
dept = a.get("department") or ""
|
|
1506
|
+
if not dept:
|
|
1507
|
+
continue
|
|
1508
|
+
row = by_dept.setdefault(dept, {
|
|
1509
|
+
"department": dept,
|
|
1510
|
+
"agent_count": 0,
|
|
1511
|
+
"tier_counts": {"0": 0, "1": 0, "2": 0, "3": 0},
|
|
1512
|
+
})
|
|
1513
|
+
row["agent_count"] += 1
|
|
1514
|
+
tier_key = str(a.get("tier") or "")
|
|
1515
|
+
if tier_key in row["tier_counts"]:
|
|
1516
|
+
row["tier_counts"][tier_key] += 1
|
|
1517
|
+
cost_map: dict[str, dict] = {}
|
|
1518
|
+
try:
|
|
1519
|
+
from core.runtime.llm_cost_telemetry import summarise
|
|
1520
|
+
s = summarise(period="month")
|
|
1521
|
+
for category, row in (s.by_category or {}).items():
|
|
1522
|
+
if not isinstance(category, str) or not category.startswith("subagent:"):
|
|
1523
|
+
continue
|
|
1524
|
+
parts = category.split(":", 2)
|
|
1525
|
+
dept = parts[1] if len(parts) >= 2 else "unknown"
|
|
1526
|
+
bucket = cost_map.setdefault(dept, {
|
|
1527
|
+
"calls_30d": 0,
|
|
1528
|
+
"cost_usd_30d": 0.0,
|
|
1529
|
+
"any_cost_known": False,
|
|
1530
|
+
})
|
|
1531
|
+
bucket["calls_30d"] += int(row.get("call_count", 0))
|
|
1532
|
+
cost = row.get("total_cost_usd")
|
|
1533
|
+
if isinstance(cost, (int, float)):
|
|
1534
|
+
bucket["cost_usd_30d"] += float(cost)
|
|
1535
|
+
bucket["any_cost_known"] = True
|
|
1536
|
+
except Exception: # noqa: BLE001
|
|
1537
|
+
pass
|
|
1538
|
+
out: list[dict] = []
|
|
1539
|
+
for dept in sorted(by_dept.keys()):
|
|
1540
|
+
row = by_dept[dept]
|
|
1541
|
+
cost = cost_map.get(dept, {})
|
|
1542
|
+
row["calls_30d"] = cost.get("calls_30d", 0)
|
|
1543
|
+
row["cost_usd_30d"] = (
|
|
1544
|
+
round(cost.get("cost_usd_30d", 0.0), 6)
|
|
1545
|
+
if cost.get("any_cost_known") else None
|
|
1546
|
+
)
|
|
1547
|
+
out.append(row)
|
|
1548
|
+
return {"departments": out, "total": len(out)}
|
|
1549
|
+
|
|
1550
|
+
|
|
1551
|
+
@app.get("/api/departments/{dept_id}")
|
|
1552
|
+
def department_detail(dept_id: str):
|
|
1553
|
+
"""Full department detail: agents, workflows, 30d cost."""
|
|
1554
|
+
dept_id = dept_id.strip().lower()
|
|
1555
|
+
agents = [a for a in _load_agents() if a.get("department") == dept_id]
|
|
1556
|
+
if not agents:
|
|
1557
|
+
return {"error": "Department not found or has no agents"}
|
|
1558
|
+
workflows: list[dict] = []
|
|
1559
|
+
try:
|
|
1560
|
+
import yaml as _yaml
|
|
1561
|
+
wf_dir = ARKAOS_ROOT / "departments" / dept_id / "workflows"
|
|
1562
|
+
if wf_dir.exists():
|
|
1563
|
+
for path in sorted(wf_dir.glob("*.yaml")):
|
|
1564
|
+
try:
|
|
1565
|
+
raw = _yaml.safe_load(path.read_text(encoding="utf-8")) or {}
|
|
1566
|
+
except Exception: # noqa: BLE001
|
|
1567
|
+
raw = {}
|
|
1568
|
+
if not isinstance(raw, dict):
|
|
1569
|
+
continue
|
|
1570
|
+
workflows.append({
|
|
1571
|
+
"id": str(raw.get("id") or path.stem),
|
|
1572
|
+
"name": str(raw.get("name") or path.stem),
|
|
1573
|
+
"tier": str(raw.get("tier") or ""),
|
|
1574
|
+
"command": str(raw.get("command") or ""),
|
|
1575
|
+
"phases_count": len(raw.get("phases") or []),
|
|
1576
|
+
})
|
|
1577
|
+
except ImportError:
|
|
1578
|
+
pass
|
|
1579
|
+
calls_30d = 0
|
|
1580
|
+
cost_usd_30d: Optional[float] = None
|
|
1581
|
+
try:
|
|
1582
|
+
from core.runtime.llm_cost_telemetry import summarise
|
|
1583
|
+
s = summarise(period="month")
|
|
1584
|
+
total_cost = 0.0
|
|
1585
|
+
any_known = False
|
|
1586
|
+
for category, row in (s.by_category or {}).items():
|
|
1587
|
+
if not isinstance(category, str) or not category.startswith("subagent:"):
|
|
1588
|
+
continue
|
|
1589
|
+
parts = category.split(":", 2)
|
|
1590
|
+
if len(parts) >= 2 and parts[1] == dept_id:
|
|
1591
|
+
calls_30d += int(row.get("call_count", 0))
|
|
1592
|
+
cost = row.get("total_cost_usd")
|
|
1593
|
+
if isinstance(cost, (int, float)):
|
|
1594
|
+
total_cost += float(cost)
|
|
1595
|
+
any_known = True
|
|
1596
|
+
cost_usd_30d = round(total_cost, 6) if any_known else None
|
|
1597
|
+
except Exception: # noqa: BLE001
|
|
1598
|
+
pass
|
|
1599
|
+
return {
|
|
1600
|
+
"department": dept_id,
|
|
1601
|
+
"agents": [
|
|
1602
|
+
{
|
|
1603
|
+
"id": a.get("id"),
|
|
1604
|
+
"name": a.get("name"),
|
|
1605
|
+
"role": a.get("role"),
|
|
1606
|
+
"tier": a.get("tier"),
|
|
1607
|
+
"mbti": a.get("mbti"),
|
|
1608
|
+
"disc": a.get("disc"),
|
|
1609
|
+
}
|
|
1610
|
+
for a in agents
|
|
1611
|
+
],
|
|
1612
|
+
"workflows": workflows,
|
|
1613
|
+
"calls_30d": calls_30d,
|
|
1614
|
+
"cost_usd_30d": cost_usd_30d,
|
|
1615
|
+
}
|
|
1616
|
+
|
|
1617
|
+
|
|
1407
1618
|
# --- Workflows (PR88b v3.24.0) ---
|
|
1408
1619
|
|
|
1409
1620
|
@app.get("/api/workflows")
|