arkaos 3.36.0 → 3.38.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.36.0
1
+ 3.38.0
@@ -53,7 +53,8 @@ interface TrendResponse {
53
53
 
54
54
  type Period = 'today' | 'week' | 'month' | 'all'
55
55
 
56
- const { fetchApi } = useApi()
56
+ const { fetchApi, apiBase } = useApi()
57
+ const toast = useToast()
57
58
  const period = ref<Period>('today')
58
59
 
59
60
  const periodOptions: { label: string; value: Period }[] = [
@@ -73,6 +74,36 @@ const {
73
74
  { query: computed(() => ({ period: period.value })) },
74
75
  )
75
76
 
77
+ // PR91d v3.38.0 — CSV export of the telemetry rows for the current period.
78
+ async function exportCsv() {
79
+ try {
80
+ const blob = await $fetch<Blob>(
81
+ `${apiBase}/api/llm-costs/export.csv`,
82
+ { query: { period: period.value }, responseType: 'blob' },
83
+ )
84
+ const url = URL.createObjectURL(blob)
85
+ const a = document.createElement('a')
86
+ a.href = url
87
+ a.download = `arkaos-costs-${period.value}.csv`
88
+ document.body.appendChild(a)
89
+ a.click()
90
+ a.remove()
91
+ URL.revokeObjectURL(url)
92
+ toast.add({
93
+ title: 'CSV downloaded',
94
+ description: `arkaos-costs-${period.value}.csv`,
95
+ color: 'success',
96
+ icon: 'i-lucide-download',
97
+ })
98
+ } catch (err) {
99
+ toast.add({
100
+ title: 'Export failed',
101
+ description: err instanceof Error ? err.message : 'unknown error',
102
+ color: 'error',
103
+ })
104
+ }
105
+ }
106
+
76
107
  // PR90c v3.33.0 — let the operator pick 7d / 14d / 30d.
77
108
  const trendDays = ref<7 | 14 | 30>(7)
78
109
  const trendDaysOptions = [
@@ -204,6 +235,14 @@ async function refreshAll() {
204
235
  size="sm"
205
236
  class="w-32"
206
237
  />
238
+ <UButton
239
+ label="Export CSV"
240
+ icon="i-lucide-download"
241
+ variant="soft"
242
+ size="sm"
243
+ class="ml-2"
244
+ @click="exportCsv"
245
+ />
207
246
  <UButton
208
247
  label="Refresh"
209
248
  variant="ghost"
@@ -7,6 +7,14 @@
7
7
 
8
8
  import type { TableColumn } from '@nuxt/ui'
9
9
 
10
+ interface WorkflowPhase {
11
+ id: string
12
+ name: string
13
+ description: string
14
+ gate_type: string
15
+ agent_count: number
16
+ }
17
+
10
18
  interface Workflow {
11
19
  id: string
12
20
  name: string
@@ -15,6 +23,7 @@ interface Workflow {
15
23
  tier: string
16
24
  command: string
17
25
  phases_count: number
26
+ phases: WorkflowPhase[]
18
27
  file: string
19
28
  content: string
20
29
  }
@@ -35,7 +44,16 @@ interface WorkflowRun {
35
44
  }
36
45
  const runs = ref<WorkflowRun[]>([])
37
46
  const runsLoading = ref(false)
38
- const sidePanelTab = ref<'yaml' | 'runs'>('yaml')
47
+ const sidePanelTab = ref<'flow' | 'yaml' | 'runs'>('flow')
48
+
49
+ function gateColor(gateType: string): 'primary' | 'warning' | 'error' | 'neutral' {
50
+ const m: Record<string, 'primary' | 'warning' | 'error' | 'neutral'> = {
51
+ user_approval: 'warning',
52
+ quality_gate: 'error',
53
+ automatic: 'primary',
54
+ }
55
+ return m[gateType] ?? 'neutral'
56
+ }
39
57
 
40
58
  async function loadRuns(id: string) {
41
59
  runsLoading.value = true
@@ -152,7 +170,7 @@ const columns: TableColumn<Workflow>[] = [
152
170
  th: 'py-2 first:rounded-l-lg last:rounded-r-lg border-y border-default first:border-l last:border-r',
153
171
  td: 'border-b border-default',
154
172
  }"
155
- @select="(row: { original: Workflow }) => { selected = row.original; sidePanelTab = 'yaml'; runs = []; loadRuns(row.original.id) }"
173
+ @select="(row: { original: Workflow }) => { selected = row.original; sidePanelTab = 'flow'; runs = []; loadRuns(row.original.id) }"
156
174
  >
157
175
  <template #name-cell="{ row }">
158
176
  <div class="min-w-0">
@@ -205,6 +223,14 @@ const columns: TableColumn<Workflow>[] = [
205
223
  {{ selected.description }}
206
224
  </p>
207
225
  <div class="flex items-center gap-1 mt-3 text-xs">
226
+ <button
227
+ type="button"
228
+ class="px-2 py-1 rounded-md transition-colors"
229
+ :class="sidePanelTab === 'flow' ? 'bg-elevated/60 text-default font-semibold' : 'text-muted hover:text-default'"
230
+ @click="sidePanelTab = 'flow'"
231
+ >
232
+ Flow
233
+ </button>
208
234
  <button
209
235
  type="button"
210
236
  class="px-2 py-1 rounded-md transition-colors"
@@ -223,7 +249,43 @@ const columns: TableColumn<Workflow>[] = [
223
249
  </button>
224
250
  </div>
225
251
  </div>
226
- <div v-if="sidePanelTab === 'yaml'" class="overflow-x-auto">
252
+ <div v-if="sidePanelTab === 'flow'" class="p-4">
253
+ <ol v-if="selected.phases.length > 0" class="relative border-l border-default ml-2 space-y-3">
254
+ <li
255
+ v-for="(ph, idx) in selected.phases"
256
+ :key="ph.id || idx"
257
+ class="ml-4"
258
+ >
259
+ <span class="absolute -left-1.5 size-3 rounded-full bg-primary border border-primary/60" />
260
+ <div class="rounded-lg border border-default p-3 bg-elevated/20">
261
+ <div class="flex items-center gap-2 flex-wrap">
262
+ <span class="font-mono text-xs text-muted">{{ idx + 1 }}.</span>
263
+ <span class="font-semibold">{{ ph.name || ph.id }}</span>
264
+ <UBadge
265
+ v-if="ph.gate_type"
266
+ :label="ph.gate_type"
267
+ :color="gateColor(ph.gate_type)"
268
+ variant="subtle"
269
+ size="xs"
270
+ />
271
+ <UBadge
272
+ v-if="ph.agent_count > 0"
273
+ :label="`${ph.agent_count} agent${ph.agent_count === 1 ? '' : 's'}`"
274
+ variant="outline"
275
+ size="xs"
276
+ />
277
+ </div>
278
+ <p v-if="ph.description" class="text-xs text-muted mt-1">
279
+ {{ ph.description }}
280
+ </p>
281
+ </div>
282
+ </li>
283
+ </ol>
284
+ <div v-else class="py-6 text-center text-sm text-muted">
285
+ No phases defined.
286
+ </div>
287
+ </div>
288
+ <div v-else-if="sidePanelTab === 'yaml'" class="overflow-x-auto">
227
289
  <pre class="p-4 text-xs font-mono whitespace-pre">{{ selected.content }}</pre>
228
290
  </div>
229
291
  <div v-else class="p-4">
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "arkaos",
3
- "version": "3.36.0",
3
+ "version": "3.38.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.36.0"
3
+ version = "3.38.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"}
@@ -1848,12 +1848,33 @@ def workflows_list():
1848
1848
  "tier": str(raw.get("tier") or ""),
1849
1849
  "command": str(raw.get("command") or ""),
1850
1850
  "phases_count": len(phases),
1851
+ "phases": _summarise_phases(phases), # PR91c v3.37.0
1851
1852
  "file": rel,
1852
1853
  "content": content,
1853
1854
  })
1854
1855
  return {"workflows": out}
1855
1856
 
1856
1857
 
1858
+ def _summarise_phases(phases: list) -> list[dict]:
1859
+ """PR91c v3.37.0 — distil each phase down to what the flow stepper needs."""
1860
+ out: list[dict] = []
1861
+ for p in phases:
1862
+ if not isinstance(p, dict):
1863
+ continue
1864
+ gate = p.get("gate") if isinstance(p.get("gate"), dict) else {}
1865
+ agents = p.get("agents") if isinstance(p.get("agents"), list) else []
1866
+ out.append({
1867
+ "id": str(p.get("id") or ""),
1868
+ "name": str(p.get("name") or ""),
1869
+ "description": str(p.get("description") or ""),
1870
+ "gate_type": str(gate.get("type") or ""),
1871
+ "agent_count": sum(
1872
+ 1 for a in agents if isinstance(a, dict) and a.get("agent_id")
1873
+ ),
1874
+ })
1875
+ return out
1876
+
1877
+
1857
1878
  # --- Sidebar stats widget (PR87d v3.22.0) ---
1858
1879
 
1859
1880
  @app.get("/api/sidebar-stats")
@@ -2489,6 +2510,56 @@ def llm_costs(period: str = "today"):
2489
2510
  }
2490
2511
 
2491
2512
 
2513
+ @app.get("/api/llm-costs/export.csv")
2514
+ def llm_costs_export(period: str = "month"):
2515
+ """PR91d v3.38.0 — stream the telemetry rows for the period as CSV.
2516
+
2517
+ Returns ``text/csv`` with a header row + every row in the selected
2518
+ period. Period values match `summarise()`: today / week / month / all.
2519
+ """
2520
+ try:
2521
+ from core.runtime.llm_cost_telemetry import (
2522
+ VALID_PERIODS,
2523
+ _load_slice,
2524
+ _period_cutoff,
2525
+ )
2526
+ except Exception:
2527
+ return {"error": "telemetry unavailable"}
2528
+ if period not in VALID_PERIODS:
2529
+ period = "month"
2530
+
2531
+ entries, _ = _load_slice(None, _period_cutoff(period, now=None))
2532
+ import csv
2533
+ import io
2534
+ buffer = io.StringIO()
2535
+ writer = csv.writer(buffer)
2536
+ writer.writerow([
2537
+ "ts", "session_id", "provider", "model", "category",
2538
+ "tokens_in", "tokens_out", "cached_tokens", "estimated_cost_usd",
2539
+ ])
2540
+ for entry in entries:
2541
+ writer.writerow([
2542
+ entry.get("ts", ""),
2543
+ entry.get("session_id", ""),
2544
+ entry.get("provider", ""),
2545
+ entry.get("model", ""),
2546
+ entry.get("category", ""),
2547
+ entry.get("tokens_in", ""),
2548
+ entry.get("tokens_out", ""),
2549
+ entry.get("cached_tokens", ""),
2550
+ entry.get("estimated_cost_usd", ""),
2551
+ ])
2552
+ filename = f"arkaos-costs-{period}.csv"
2553
+ from fastapi import Response
2554
+ return Response(
2555
+ content=buffer.getvalue(),
2556
+ media_type="text/csv",
2557
+ headers={
2558
+ "Content-Disposition": f'attachment; filename="{filename}"',
2559
+ },
2560
+ )
2561
+
2562
+
2492
2563
  @app.get("/api/llm-costs/trend")
2493
2564
  def llm_costs_trend(days: int = 7):
2494
2565
  """Day-by-day rolling totals from the cost telemetry.