@cat-factory/app 0.232.2 → 0.233.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/app/components/observability/OutcomeFilterChips.vue +41 -0
- package/app/components/observability/RunFailureSummary.vue +243 -0
- package/app/components/observability/ToolCallList.vue +290 -0
- package/app/components/panels/ObservabilityPanel.vue +396 -129
- package/app/composables/api/execution.ts +22 -0
- package/app/stores/observability/toolCalls.ts +173 -0
- package/app/stores/observability.ts +13 -0
- package/app/types/execution.ts +6 -0
- package/app/utils/observability.spec.ts +313 -2
- package/app/utils/observability.ts +232 -1
- package/i18n/locales/de.json +45 -0
- package/i18n/locales/en.json +45 -0
- package/i18n/locales/es.json +45 -0
- package/i18n/locales/fr.json +45 -0
- package/i18n/locales/he.json +45 -0
- package/i18n/locales/it.json +45 -0
- package/i18n/locales/ja.json +45 -0
- package/i18n/locales/pl.json +45 -0
- package/i18n/locales/tr.json +45 -0
- package/i18n/locales/uk.json +45 -0
- package/package.json +2 -2
|
@@ -1,22 +1,40 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
|
-
import { computed, reactive, ref, watch } from 'vue'
|
|
2
|
+
import { computed, nextTick, reactive, ref, watch } from 'vue'
|
|
3
3
|
import { onKeyStroke } from '@vueuse/core'
|
|
4
|
+
import { isLlmWarningFinishReason } from '@cat-factory/contracts'
|
|
4
5
|
import type {
|
|
5
6
|
AgentContextSnapshot,
|
|
6
7
|
AgentSearchQuery,
|
|
7
8
|
LlmCallMetric,
|
|
9
|
+
RunToolCallFailures,
|
|
10
|
+
RunToolCallTrajectory,
|
|
8
11
|
WebSearchProvider,
|
|
9
12
|
} from '~/types/execution'
|
|
10
13
|
import { agentKindMeta } from '~/utils/catalog'
|
|
14
|
+
import type { CallOutcomeFilter, ToolOutcomeFilter } from '~/utils/observability'
|
|
11
15
|
import {
|
|
16
|
+
countCallOutcomes,
|
|
17
|
+
deriveRunFailureEvidence,
|
|
18
|
+
filterCallsByOutcome,
|
|
12
19
|
foldRunPhaseMetrics,
|
|
13
20
|
formatCost,
|
|
14
21
|
formatMs,
|
|
15
22
|
formatTokens,
|
|
23
|
+
hasFailureEvidence,
|
|
16
24
|
pct,
|
|
25
|
+
sinkAnswer,
|
|
17
26
|
sumCosts,
|
|
18
27
|
totalInputTokens,
|
|
19
28
|
} from '~/utils/observability'
|
|
29
|
+
import OutcomeFilterChips from '~/components/observability/OutcomeFilterChips.vue'
|
|
30
|
+
import RunFailureSummary from '~/components/observability/RunFailureSummary.vue'
|
|
31
|
+
import ToolCallList from '~/components/observability/ToolCallList.vue'
|
|
32
|
+
|
|
33
|
+
/** No run selected: the same empty, NOT-truncated trajectory the store answers with. */
|
|
34
|
+
const EMPTY_TRAJECTORY: RunToolCallTrajectory = Object.freeze({
|
|
35
|
+
toolCalls: Object.freeze([]) as never,
|
|
36
|
+
truncated: false,
|
|
37
|
+
})
|
|
20
38
|
|
|
21
39
|
// Drill-down overlay for a run's LLM activity. Opened via
|
|
22
40
|
// `ui.openObservability(instanceId)` from a step surface; loads the full per-call
|
|
@@ -24,6 +42,11 @@ import {
|
|
|
24
42
|
// transport-vs-execution latency split) from the observability store and lists
|
|
25
43
|
// every model call, each expandable to its full prompt + response. Offers the
|
|
26
44
|
// LLM-friendly JSON export for handing a run to a model to analyse.
|
|
45
|
+
//
|
|
46
|
+
// Failing-call FIRST: when the run failed (or any call did), the panel opens with a pinned
|
|
47
|
+
// summary naming the structured failure and the two calls that actually failed, so the cause is
|
|
48
|
+
// visible before anything is read. The lists below it narrow by outcome for the same reason:
|
|
49
|
+
// a tool-execution error is a row nothing aggregates, so finding one used to mean scrolling.
|
|
27
50
|
const ui = useUiStore()
|
|
28
51
|
const execution = useExecutionStore()
|
|
29
52
|
const board = useBoardStore()
|
|
@@ -56,9 +79,9 @@ function retryContext() {
|
|
|
56
79
|
if (executionId.value) void observability.loadContext(executionId.value)
|
|
57
80
|
}
|
|
58
81
|
|
|
59
|
-
// Which view is shown: per-call model activity, the
|
|
60
|
-
// performed web searches.
|
|
61
|
-
const view = ref<'calls' | 'context' | 'search'>('calls')
|
|
82
|
+
// Which view is shown: per-call model activity, the tool-call trajectory, the complete provided
|
|
83
|
+
// context, or the performed web searches.
|
|
84
|
+
const view = ref<'calls' | 'tools' | 'context' | 'search'>('calls')
|
|
62
85
|
|
|
63
86
|
const contextSnapshots = computed<AgentContextSnapshot[]>(() =>
|
|
64
87
|
executionId.value ? observability.contextFor(executionId.value) : [],
|
|
@@ -74,6 +97,171 @@ const searchLoading = computed(
|
|
|
74
97
|
() => !!executionId.value && observability.isSearchQueriesLoading(executionId.value),
|
|
75
98
|
)
|
|
76
99
|
|
|
100
|
+
// The tool-call sink is read TWICE, at two different bounds, and the split is the point.
|
|
101
|
+
//
|
|
102
|
+
// `toolFailures` is the run-level answer: exact `{ total, failed }` counted in SQL plus the
|
|
103
|
+
// failing rows themselves, cheap enough to front the panel. `trajectory` is the browse view: a
|
|
104
|
+
// bounded prefix carrying every argument and result the run captured, loaded only when someone
|
|
105
|
+
// opens it. Folding them into one read would either make the headline wait on megabytes or make
|
|
106
|
+
// its numbers a statement about the prefix, and the second one is a false all-clear.
|
|
107
|
+
const trajectory = computed<RunToolCallTrajectory>(() =>
|
|
108
|
+
executionId.value ? observability.toolCallsFor(executionId.value) : EMPTY_TRAJECTORY,
|
|
109
|
+
)
|
|
110
|
+
const toolCallsLoading = computed(
|
|
111
|
+
() => !!executionId.value && observability.isToolCallsLoading(executionId.value),
|
|
112
|
+
)
|
|
113
|
+
const toolCallError = computed(() =>
|
|
114
|
+
executionId.value ? (observability.toolCallErrors[executionId.value] ?? null) : null,
|
|
115
|
+
)
|
|
116
|
+
const toolFailures = computed<RunToolCallFailures | null>(() =>
|
|
117
|
+
executionId.value ? observability.toolCallFailuresFor(executionId.value) : null,
|
|
118
|
+
)
|
|
119
|
+
const toolFailuresLoading = computed(
|
|
120
|
+
() => !!executionId.value && observability.isToolCallFailuresLoading(executionId.value),
|
|
121
|
+
)
|
|
122
|
+
const toolFailureError = computed(() =>
|
|
123
|
+
executionId.value ? (observability.toolCallFailureErrors[executionId.value] ?? null) : null,
|
|
124
|
+
)
|
|
125
|
+
function retryToolCalls() {
|
|
126
|
+
if (executionId.value) void observability.loadToolCalls(executionId.value)
|
|
127
|
+
}
|
|
128
|
+
function retryToolCallFailures() {
|
|
129
|
+
if (executionId.value) void observability.loadToolCallFailures(executionId.value)
|
|
130
|
+
}
|
|
131
|
+
/** Re-request whatever the pinned summary could not read. Both, when both failed. */
|
|
132
|
+
function retryFailureEvidence() {
|
|
133
|
+
const id = executionId.value
|
|
134
|
+
if (!id) return
|
|
135
|
+
if (observability.toolCallFailureErrors[id]) void observability.loadToolCallFailures(id)
|
|
136
|
+
if (observability.errors[id]) void observability.load(id)
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Show the trajectory tab, loading it if this is the first look.
|
|
141
|
+
*
|
|
142
|
+
* A named handler rather than two statements in the template: an inline handler is parsed as a
|
|
143
|
+
* single expression, so the multi-statement form is a build-time syntax error that neither the
|
|
144
|
+
* typecheck nor the unit tests compile a template to catch.
|
|
145
|
+
*/
|
|
146
|
+
function openToolsView() {
|
|
147
|
+
view.value = 'tools'
|
|
148
|
+
ensureTrajectoryLoaded()
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Load the trajectory the first time it is actually looked at.
|
|
153
|
+
*
|
|
154
|
+
* Deferred because it is the one read on this panel whose size scales with how much the run DID
|
|
155
|
+
* rather than with how it ended, and an operator who opens the panel to see what broke may never
|
|
156
|
+
* scroll it. What they do see immediately is the failure read, which is issued on open.
|
|
157
|
+
*/
|
|
158
|
+
function ensureTrajectoryLoaded() {
|
|
159
|
+
const id = executionId.value
|
|
160
|
+
if (!id || observability.hasToolCalls(id) || observability.isToolCallsLoading(id)) return
|
|
161
|
+
void observability.loadToolCalls(id)
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// --- failing-call-first triage ------------------------------------------------------------
|
|
165
|
+
// Both drill-downs narrow by outcome. The state lives HERE rather than in each list so the
|
|
166
|
+
// pinned summary's "show me the failing tool calls" can set it, and so switching views does not
|
|
167
|
+
// silently drop a narrowing the operator is still reading under.
|
|
168
|
+
const callFilter = ref<CallOutcomeFilter>('all')
|
|
169
|
+
const toolFilter = ref<ToolOutcomeFilter>('all')
|
|
170
|
+
|
|
171
|
+
const callOutcomeCounts = computed(() => countCallOutcomes(calls.value))
|
|
172
|
+
const callFilterOptions = computed(
|
|
173
|
+
() =>
|
|
174
|
+
[
|
|
175
|
+
{ value: 'all', label: t('observability.filter.all'), count: callOutcomeCounts.value.all },
|
|
176
|
+
{
|
|
177
|
+
value: 'error',
|
|
178
|
+
label: t('observability.filter.failed'),
|
|
179
|
+
count: callOutcomeCounts.value.error,
|
|
180
|
+
tone: 'error',
|
|
181
|
+
},
|
|
182
|
+
{
|
|
183
|
+
value: 'warning',
|
|
184
|
+
label: t('observability.filter.warning'),
|
|
185
|
+
count: callOutcomeCounts.value.warning,
|
|
186
|
+
tone: 'warning',
|
|
187
|
+
},
|
|
188
|
+
{ value: 'ok', label: t('observability.filter.ok'), count: callOutcomeCounts.value.ok },
|
|
189
|
+
] as const,
|
|
190
|
+
)
|
|
191
|
+
/** The rows the call list actually renders, after the outcome narrowing. */
|
|
192
|
+
const visibleCalls = computed(() => filterCallsByOutcome(calls.value, callFilter.value))
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* What the panel pins at the top: the run's structured failure record plus the last call that
|
|
196
|
+
* failed in each sink. Sharpens as each read lands rather than blocking on both.
|
|
197
|
+
*
|
|
198
|
+
* Each sink is passed its own ANSWER, not merely its rows, because zero rows is what a loading
|
|
199
|
+
* read, a failed read, an unwired sink and a genuinely quiet run all look like from here, and
|
|
200
|
+
* only the first two must never be rendered as "nothing failed".
|
|
201
|
+
*/
|
|
202
|
+
const failureEvidence = computed(() =>
|
|
203
|
+
deriveRunFailureEvidence({
|
|
204
|
+
failure: instance.value?.failure ?? null,
|
|
205
|
+
calls: calls.value,
|
|
206
|
+
callsAnswer: sinkAnswer({
|
|
207
|
+
loading: loading.value,
|
|
208
|
+
error: error.value,
|
|
209
|
+
loaded: !!executionId.value && executionId.value in observability.callsByExecution,
|
|
210
|
+
rows: calls.value.length,
|
|
211
|
+
}),
|
|
212
|
+
toolFailures: toolFailures.value,
|
|
213
|
+
toolsAnswer: sinkAnswer({
|
|
214
|
+
loading: toolFailuresLoading.value,
|
|
215
|
+
error: toolFailureError.value,
|
|
216
|
+
loaded: !!toolFailures.value,
|
|
217
|
+
rows: toolFailures.value?.total ?? 0,
|
|
218
|
+
}),
|
|
219
|
+
}),
|
|
220
|
+
)
|
|
221
|
+
/**
|
|
222
|
+
* Which call rows are expanded.
|
|
223
|
+
*
|
|
224
|
+
* Declared here rather than beside `toggle` below because `revealCall` writes it: the pinned
|
|
225
|
+
* summary's jump opens the row it scrolls to, so the state has to exist above its first use.
|
|
226
|
+
*/
|
|
227
|
+
const expanded = reactive<Record<string, boolean>>({})
|
|
228
|
+
|
|
229
|
+
/**
|
|
230
|
+
* Whether to pin the section at all.
|
|
231
|
+
*
|
|
232
|
+
* Deliberately NOT gated on `status === 'failed'`: a run still in flight whose calls are already
|
|
233
|
+
* erroring is exactly the one worth interrupting, and a run that ended `done` after recovering
|
|
234
|
+
* from a failure still has the failures worth reading. What the section never does is appear
|
|
235
|
+
* with nothing to say: `hasFailureEvidence` is false when there is no record and nothing failed.
|
|
236
|
+
*/
|
|
237
|
+
const showFailureSummary = computed(() => hasFailureEvidence(failureEvidence.value))
|
|
238
|
+
|
|
239
|
+
/** Open one call's row in the list below, expanded, from the pinned summary. */
|
|
240
|
+
async function revealCall(callId: string) {
|
|
241
|
+
view.value = 'calls'
|
|
242
|
+
// Clear a narrowing that would hide the row we are about to scroll to. Every filter but
|
|
243
|
+
// `error` can do that, and a jump to a row the list is not rendering silently does nothing.
|
|
244
|
+
if (callFilter.value !== 'all' && callFilter.value !== 'error') callFilter.value = 'all'
|
|
245
|
+
expanded[callId] = true
|
|
246
|
+
await nextTick()
|
|
247
|
+
document.getElementById(callRowId(callId))?.scrollIntoView({ block: 'center' })
|
|
248
|
+
}
|
|
249
|
+
/**
|
|
250
|
+
* Open the trajectory narrowed to the failures, from the pinned summary.
|
|
251
|
+
*
|
|
252
|
+
* The failing rows are already in hand (they come from the failure read), so this renders
|
|
253
|
+
* immediately; the prefix load it kicks off is what fills in the surrounding calls an operator
|
|
254
|
+
* widens to when they want the context around one.
|
|
255
|
+
*/
|
|
256
|
+
function revealFailingToolCalls() {
|
|
257
|
+
view.value = 'tools'
|
|
258
|
+
toolFilter.value = 'error'
|
|
259
|
+
ensureTrajectoryLoaded()
|
|
260
|
+
}
|
|
261
|
+
function callRowId(callId: string): string {
|
|
262
|
+
return `obs-call-${callId}`
|
|
263
|
+
}
|
|
264
|
+
|
|
77
265
|
// Brand names, kept verbatim across locales (not translatable prose).
|
|
78
266
|
const PROVIDER_LABEL: Record<WebSearchProvider, string> = { brave: 'Brave', searxng: 'SearXNG' }
|
|
79
267
|
function providerLabel(provider: WebSearchProvider | null): string {
|
|
@@ -107,9 +295,16 @@ watch(
|
|
|
107
295
|
(id) => {
|
|
108
296
|
if (id) {
|
|
109
297
|
view.value = 'calls'
|
|
298
|
+
callFilter.value = 'all'
|
|
299
|
+
toolFilter.value = 'all'
|
|
110
300
|
void observability.load(id)
|
|
111
301
|
void observability.loadContext(id)
|
|
112
302
|
void observability.loadSearchQueries(id)
|
|
303
|
+
// The FAILURE read on open, the trajectory on demand. This one is what the pinned summary
|
|
304
|
+
// speaks from — the failure class no other number on the panel reveals — and it is two
|
|
305
|
+
// aggregates and a handful of rows, so the headline answer costs a tab-click less than the
|
|
306
|
+
// browse view it used to ride along with.
|
|
307
|
+
void observability.loadToolCallFailures(id)
|
|
113
308
|
}
|
|
114
309
|
},
|
|
115
310
|
// Lazy v-if mount: the panel mounts with executionId already set, so load immediately.
|
|
@@ -209,11 +404,17 @@ function carryShare(carryCostTokens: number): number | null {
|
|
|
209
404
|
function phaseLabel(phase: string): string {
|
|
210
405
|
return phase || t('observability.phase.unattributed')
|
|
211
406
|
}
|
|
407
|
+
/**
|
|
408
|
+
* Whether a successful call's finish reason is a warning (cut short, or filtered).
|
|
409
|
+
*
|
|
410
|
+
* The rule itself lives in `@cat-factory/contracts` beside the backend's own classification: a
|
|
411
|
+
* hand-copied list here was fine while it only picked a badge colour, and stopped being fine the
|
|
412
|
+
* moment the outcome FILTER decides which rows the operator is shown.
|
|
413
|
+
*/
|
|
212
414
|
function isWarning(finishReason: string | null): boolean {
|
|
213
|
-
return finishReason
|
|
415
|
+
return isLlmWarningFinishReason(finishReason)
|
|
214
416
|
}
|
|
215
417
|
|
|
216
|
-
const expanded = reactive<Record<string, boolean>>({})
|
|
217
418
|
function toggle(c: LlmCallMetric) {
|
|
218
419
|
expanded[c.id] = !expanded[c.id]
|
|
219
420
|
// A live-streamed row arrives without its prompt/response bodies (the event stays
|
|
@@ -288,6 +489,17 @@ function exportJson() {
|
|
|
288
489
|
>
|
|
289
490
|
{{ t('observability.modelActivity') }}
|
|
290
491
|
</button>
|
|
492
|
+
<button
|
|
493
|
+
class="rounded-md px-2.5 py-1 transition"
|
|
494
|
+
:class="
|
|
495
|
+
view === 'tools'
|
|
496
|
+
? 'bg-slate-800 text-slate-100'
|
|
497
|
+
: 'text-slate-400 hover:text-slate-200'
|
|
498
|
+
"
|
|
499
|
+
@click="openToolsView()"
|
|
500
|
+
>
|
|
501
|
+
{{ t('observability.toolCalls.title') }}
|
|
502
|
+
</button>
|
|
291
503
|
<button
|
|
292
504
|
class="rounded-md px-2.5 py-1 transition"
|
|
293
505
|
:class="
|
|
@@ -337,6 +549,17 @@ function exportJson() {
|
|
|
337
549
|
|
|
338
550
|
<div class="flex-1 overflow-auto px-6 py-6">
|
|
339
551
|
<div v-if="view === 'calls'" class="mx-auto max-w-4xl space-y-5">
|
|
552
|
+
<!-- What broke, pinned ABOVE everything: the structured failure record plus the last
|
|
553
|
+
call that failed in each sink. The point of the panel's first screen is that the
|
|
554
|
+
cause is read, not hunted. -->
|
|
555
|
+
<RunFailureSummary
|
|
556
|
+
v-if="showFailureSummary"
|
|
557
|
+
:evidence="failureEvidence"
|
|
558
|
+
@show-call="revealCall"
|
|
559
|
+
@show-failing-tools="revealFailingToolCalls"
|
|
560
|
+
@retry="retryFailureEvidence"
|
|
561
|
+
/>
|
|
562
|
+
|
|
340
563
|
<!-- run-level summary -->
|
|
341
564
|
<section class="rounded-xl border border-slate-800 bg-slate-900/50 p-4">
|
|
342
565
|
<dl class="grid grid-cols-2 gap-x-6 gap-y-3 text-[13px] sm:grid-cols-4">
|
|
@@ -576,144 +799,188 @@ function exportJson() {
|
|
|
576
799
|
{{ t('observability.noCalls') }}
|
|
577
800
|
</p>
|
|
578
801
|
|
|
579
|
-
<!-- per-call list -->
|
|
580
|
-
<
|
|
581
|
-
<
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
802
|
+
<!-- per-call list, narrowable by outcome -->
|
|
803
|
+
<template v-else>
|
|
804
|
+
<div class="flex flex-wrap items-center justify-between gap-2">
|
|
805
|
+
<h2 class="text-[11px] uppercase tracking-wide text-slate-500">
|
|
806
|
+
{{ t('observability.callsTitle') }}
|
|
807
|
+
</h2>
|
|
808
|
+
<OutcomeFilterChips v-model="callFilter" :options="callFilterOptions" />
|
|
809
|
+
</div>
|
|
810
|
+
|
|
811
|
+
<!-- Narrowed to nothing reads differently from recorded nothing, and on this
|
|
812
|
+
surface it is the good news: the operator asked for the failures and there
|
|
813
|
+
are none. -->
|
|
814
|
+
<p
|
|
815
|
+
v-if="!visibleCalls.length"
|
|
816
|
+
class="rounded-lg border border-dashed border-slate-800 py-8 text-center text-sm text-slate-500"
|
|
586
817
|
>
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
818
|
+
{{ t('observability.noCallsMatching') }}
|
|
819
|
+
</p>
|
|
820
|
+
|
|
821
|
+
<ul v-else class="space-y-2">
|
|
822
|
+
<li
|
|
823
|
+
v-for="c in visibleCalls"
|
|
824
|
+
:id="callRowId(c.id)"
|
|
825
|
+
:key="c.id"
|
|
826
|
+
class="overflow-hidden rounded-xl border border-slate-800 bg-slate-900/40"
|
|
827
|
+
:class="!c.ok ? 'border-rose-900/60' : ''"
|
|
590
828
|
>
|
|
591
|
-
<
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
:class="expanded[c.id] ? 'rotate-90' : ''"
|
|
595
|
-
/>
|
|
596
|
-
<UIcon
|
|
597
|
-
:name="agentMeta(c.agentKind).icon"
|
|
598
|
-
class="h-4 w-4 shrink-0"
|
|
599
|
-
:style="{ color: agentMeta(c.agentKind).color }"
|
|
600
|
-
/>
|
|
601
|
-
<span class="text-[13px] text-slate-200">{{ agentMeta(c.agentKind).label }}</span>
|
|
602
|
-
<span
|
|
603
|
-
class="hidden truncate text-[11px] text-slate-500 sm:inline"
|
|
604
|
-
:title="c.model"
|
|
829
|
+
<button
|
|
830
|
+
class="flex w-full items-center gap-3 px-4 py-2.5 text-start transition hover:bg-slate-900/70"
|
|
831
|
+
@click="toggle(c)"
|
|
605
832
|
>
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
<
|
|
612
|
-
:
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
>
|
|
620
|
-
{{ formatTokens(totalInputTokens(c)) }}↑
|
|
621
|
-
{{ formatTokens(c.completionTokens) }}↓
|
|
622
|
-
</span>
|
|
833
|
+
<UIcon
|
|
834
|
+
name="i-lucide-chevron-right"
|
|
835
|
+
class="h-4 w-4 shrink-0 text-slate-500 transition-transform"
|
|
836
|
+
:class="expanded[c.id] ? 'rotate-90' : ''"
|
|
837
|
+
/>
|
|
838
|
+
<UIcon
|
|
839
|
+
:name="agentMeta(c.agentKind).icon"
|
|
840
|
+
class="h-4 w-4 shrink-0"
|
|
841
|
+
:style="{ color: agentMeta(c.agentKind).color }"
|
|
842
|
+
/>
|
|
843
|
+
<span class="text-[13px] text-slate-200">{{
|
|
844
|
+
agentMeta(c.agentKind).label
|
|
845
|
+
}}</span>
|
|
623
846
|
<span
|
|
624
|
-
|
|
625
|
-
:title="
|
|
847
|
+
class="hidden truncate text-[11px] text-slate-500 sm:inline"
|
|
848
|
+
:title="c.model"
|
|
626
849
|
>
|
|
627
|
-
{{
|
|
850
|
+
{{ c.provider }}:{{ c.model }}
|
|
628
851
|
</span>
|
|
629
|
-
<span :title="t('observability.call.transportVsExecution')">
|
|
630
|
-
{{ formatMs(c.overheadMs) }} / {{ formatMs(c.upstreamMs) }}
|
|
631
|
-
</span>
|
|
632
|
-
<UBadge v-if="!c.ok" color="error" variant="subtle" size="sm">
|
|
633
|
-
{{ c.httpStatus ?? t('observability.call.error') }}
|
|
634
|
-
</UBadge>
|
|
635
|
-
<UBadge
|
|
636
|
-
v-else-if="isWarning(c.finishReason)"
|
|
637
|
-
color="warning"
|
|
638
|
-
variant="subtle"
|
|
639
|
-
size="sm"
|
|
640
|
-
>
|
|
641
|
-
{{ c.finishReason }}
|
|
642
|
-
</UBadge>
|
|
643
|
-
<span v-else class="text-slate-600">{{
|
|
644
|
-
c.finishReason ?? t('observability.call.ok')
|
|
645
|
-
}}</span>
|
|
646
|
-
<span class="hidden text-slate-600 md:inline">{{ clock(c.createdAt) }}</span>
|
|
647
|
-
</div>
|
|
648
|
-
</button>
|
|
649
|
-
|
|
650
|
-
<div v-if="expanded[c.id]" class="border-t border-slate-800 px-4 py-3 space-y-3">
|
|
651
|
-
<p v-if="c.errorMessage" class="text-[12px] text-rose-400">
|
|
652
|
-
{{ c.errorMessage }}
|
|
653
|
-
</p>
|
|
654
|
-
<div class="flex flex-wrap gap-x-5 gap-y-1 text-[11px] text-slate-500">
|
|
655
|
-
<span>{{ t('observability.call.messages', { count: c.messageCount }) }}</span>
|
|
656
|
-
<span>{{ t('observability.call.tools', { count: c.toolCount }) }}</span>
|
|
657
|
-
<span>{{
|
|
658
|
-
c.streaming
|
|
659
|
-
? t('observability.call.streamed')
|
|
660
|
-
: t('observability.call.buffered')
|
|
661
|
-
}}</span>
|
|
662
|
-
<span v-if="c.requestMaxTokens != null">{{
|
|
663
|
-
t('observability.call.maxTokens', { value: c.requestMaxTokens })
|
|
664
|
-
}}</span>
|
|
665
|
-
<span v-if="c.cacheReadTokens > 0 || c.cacheWriteTokens > 0">{{
|
|
666
|
-
t('observability.call.fresh', { tokens: c.promptTokens })
|
|
667
|
-
}}</span>
|
|
668
|
-
<span v-if="c.cacheReadTokens > 0" class="text-emerald-400">{{
|
|
669
|
-
t('observability.call.cacheRead', { tokens: c.cacheReadTokens })
|
|
670
|
-
}}</span>
|
|
671
|
-
<span v-if="c.cacheWriteTokens > 0" class="text-amber-400">{{
|
|
672
|
-
t('observability.call.cacheWrite', { tokens: c.cacheWriteTokens })
|
|
673
|
-
}}</span>
|
|
674
|
-
<span>{{
|
|
675
|
-
t('observability.call.total', { duration: formatMs(c.totalMs) })
|
|
676
|
-
}}</span>
|
|
677
|
-
</div>
|
|
678
|
-
<div>
|
|
679
852
|
<div
|
|
680
|
-
class="
|
|
853
|
+
class="ms-auto flex items-center gap-2.5 text-[11px] tabular-nums text-slate-400"
|
|
681
854
|
>
|
|
682
|
-
<span>{{ t('observability.call.prompt') }}</span>
|
|
683
855
|
<span
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
count: c.promptPrefixCount,
|
|
856
|
+
:title="
|
|
857
|
+
t('observability.call.tokensTitle', {
|
|
858
|
+
input: totalInputTokens(c),
|
|
859
|
+
fresh: c.promptTokens,
|
|
860
|
+
completion: c.completionTokens,
|
|
690
861
|
})
|
|
691
|
-
|
|
862
|
+
"
|
|
863
|
+
>
|
|
864
|
+
{{ formatTokens(totalInputTokens(c)) }}↑
|
|
865
|
+
{{ formatTokens(c.completionTokens) }}↓
|
|
866
|
+
</span>
|
|
867
|
+
<span
|
|
868
|
+
v-if="headroomOf(c) !== null"
|
|
869
|
+
:title="t('observability.call.outputUsedVsLimit')"
|
|
870
|
+
>
|
|
871
|
+
{{ headroomOf(c) }}%
|
|
872
|
+
</span>
|
|
873
|
+
<span :title="t('observability.call.transportVsExecution')">
|
|
874
|
+
{{ formatMs(c.overheadMs) }} / {{ formatMs(c.upstreamMs) }}
|
|
692
875
|
</span>
|
|
876
|
+
<UBadge v-if="!c.ok" color="error" variant="subtle" size="sm">
|
|
877
|
+
{{ c.httpStatus ?? t('observability.call.error') }}
|
|
878
|
+
</UBadge>
|
|
879
|
+
<UBadge
|
|
880
|
+
v-else-if="isWarning(c.finishReason)"
|
|
881
|
+
color="warning"
|
|
882
|
+
variant="subtle"
|
|
883
|
+
size="sm"
|
|
884
|
+
>
|
|
885
|
+
{{ c.finishReason }}
|
|
886
|
+
</UBadge>
|
|
887
|
+
<span v-else class="text-slate-600">{{
|
|
888
|
+
c.finishReason ?? t('observability.call.ok')
|
|
889
|
+
}}</span>
|
|
890
|
+
<span class="hidden text-slate-600 md:inline">{{ clock(c.createdAt) }}</span>
|
|
693
891
|
</div>
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
892
|
+
</button>
|
|
893
|
+
|
|
894
|
+
<div v-if="expanded[c.id]" class="border-t border-slate-800 px-4 py-3 space-y-3">
|
|
895
|
+
<p v-if="c.errorMessage" class="text-[12px] text-rose-400">
|
|
896
|
+
{{ c.errorMessage }}
|
|
897
|
+
</p>
|
|
898
|
+
<div class="flex flex-wrap gap-x-5 gap-y-1 text-[11px] text-slate-500">
|
|
899
|
+
<span>{{ t('observability.call.messages', { count: c.messageCount }) }}</span>
|
|
900
|
+
<span>{{ t('observability.call.tools', { count: c.toolCount }) }}</span>
|
|
901
|
+
<span>{{
|
|
902
|
+
c.streaming
|
|
903
|
+
? t('observability.call.streamed')
|
|
904
|
+
: t('observability.call.buffered')
|
|
905
|
+
}}</span>
|
|
906
|
+
<span v-if="c.requestMaxTokens != null">{{
|
|
907
|
+
t('observability.call.maxTokens', { value: c.requestMaxTokens })
|
|
908
|
+
}}</span>
|
|
909
|
+
<span v-if="c.cacheReadTokens > 0 || c.cacheWriteTokens > 0">{{
|
|
910
|
+
t('observability.call.fresh', { tokens: c.promptTokens })
|
|
911
|
+
}}</span>
|
|
912
|
+
<span v-if="c.cacheReadTokens > 0" class="text-emerald-400">{{
|
|
913
|
+
t('observability.call.cacheRead', { tokens: c.cacheReadTokens })
|
|
914
|
+
}}</span>
|
|
915
|
+
<span v-if="c.cacheWriteTokens > 0" class="text-amber-400">{{
|
|
916
|
+
t('observability.call.cacheWrite', { tokens: c.cacheWriteTokens })
|
|
917
|
+
}}</span>
|
|
918
|
+
<span>{{
|
|
919
|
+
t('observability.call.total', { duration: formatMs(c.totalMs) })
|
|
920
|
+
}}</span>
|
|
701
921
|
</div>
|
|
702
|
-
<
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
922
|
+
<div>
|
|
923
|
+
<div
|
|
924
|
+
class="mb-1 flex items-center gap-2 text-[11px] uppercase tracking-wide text-slate-500"
|
|
925
|
+
>
|
|
926
|
+
<span>{{ t('observability.call.prompt') }}</span>
|
|
927
|
+
<span
|
|
928
|
+
v-if="c.promptPrefixCount > 0"
|
|
929
|
+
class="normal-case tracking-normal text-slate-600"
|
|
930
|
+
>
|
|
931
|
+
{{
|
|
932
|
+
t('observability.call.promptPrefixOmitted', {
|
|
933
|
+
count: c.promptPrefixCount,
|
|
934
|
+
})
|
|
935
|
+
}}
|
|
936
|
+
</span>
|
|
937
|
+
</div>
|
|
938
|
+
<pre
|
|
939
|
+
class="max-h-72 overflow-auto rounded-lg bg-slate-950/70 p-3 text-[11px] leading-relaxed text-slate-300"
|
|
940
|
+
>{{ prettyPrompt(c.promptText) }}</pre>
|
|
941
|
+
</div>
|
|
942
|
+
<div>
|
|
943
|
+
<div class="mb-1 text-[11px] uppercase tracking-wide text-slate-500">
|
|
944
|
+
{{ t('observability.call.response') }}
|
|
945
|
+
</div>
|
|
946
|
+
<pre
|
|
947
|
+
class="max-h-72 overflow-auto rounded-lg bg-slate-950/70 p-3 text-[11px] leading-relaxed text-slate-300"
|
|
948
|
+
>{{ c.responseText || '—' }}</pre>
|
|
949
|
+
</div>
|
|
950
|
+
<div v-if="c.reasoningText">
|
|
951
|
+
<div class="mb-1 text-[11px] uppercase tracking-wide text-slate-500">
|
|
952
|
+
{{ t('observability.call.reasoning') }}
|
|
953
|
+
</div>
|
|
954
|
+
<pre
|
|
955
|
+
class="max-h-72 overflow-auto rounded-lg bg-slate-950/70 p-3 text-[11px] leading-relaxed text-slate-400"
|
|
956
|
+
>{{ c.reasoningText }}</pre>
|
|
709
957
|
</div>
|
|
710
|
-
<pre
|
|
711
|
-
class="max-h-72 overflow-auto rounded-lg bg-slate-950/70 p-3 text-[11px] leading-relaxed text-slate-400"
|
|
712
|
-
>{{ c.reasoningText }}</pre>
|
|
713
958
|
</div>
|
|
714
|
-
</
|
|
715
|
-
</
|
|
716
|
-
</
|
|
959
|
+
</li>
|
|
960
|
+
</ul>
|
|
961
|
+
</template>
|
|
962
|
+
</div>
|
|
963
|
+
|
|
964
|
+
<!-- Tool-call trajectory: what the run's agents DID, in the order they did it. -->
|
|
965
|
+
<div v-else-if="view === 'tools'" class="mx-auto max-w-4xl space-y-5">
|
|
966
|
+
<RunFailureSummary
|
|
967
|
+
v-if="showFailureSummary"
|
|
968
|
+
:evidence="failureEvidence"
|
|
969
|
+
@show-call="revealCall"
|
|
970
|
+
@show-failing-tools="revealFailingToolCalls"
|
|
971
|
+
@retry="retryFailureEvidence"
|
|
972
|
+
/>
|
|
973
|
+
<ToolCallList
|
|
974
|
+
v-model:filter="toolFilter"
|
|
975
|
+
:trajectory="trajectory"
|
|
976
|
+
:failures="toolFailures"
|
|
977
|
+
:loading="toolCallsLoading"
|
|
978
|
+
:error="toolCallError"
|
|
979
|
+
:failures-loading="toolFailuresLoading"
|
|
980
|
+
:failures-error="toolFailureError"
|
|
981
|
+
@retry="retryToolCalls"
|
|
982
|
+
@retry-failures="retryToolCallFailures"
|
|
983
|
+
/>
|
|
717
984
|
</div>
|
|
718
985
|
|
|
719
986
|
<!-- Provided context: the complete context each container agent was given. -->
|
|
@@ -5,6 +5,8 @@ import {
|
|
|
5
5
|
getExecutionAgentContextContract,
|
|
6
6
|
getExecutionLlmMetricsContract,
|
|
7
7
|
getExecutionSearchQueriesContract,
|
|
8
|
+
getExecutionToolCallFailuresContract,
|
|
9
|
+
getExecutionToolCallsContract,
|
|
8
10
|
getWorkspaceUsageContract,
|
|
9
11
|
mergeBlockContract,
|
|
10
12
|
rejectStepContract,
|
|
@@ -165,6 +167,26 @@ export function executionApi({ send, sendWith, ws, pwHeaders }: ApiContext) {
|
|
|
165
167
|
pathParams: { executionId },
|
|
166
168
|
}),
|
|
167
169
|
|
|
170
|
+
// The tool-call trajectory: what the run's agents DID, oldest first. The half of a
|
|
171
|
+
// failure no model call reports: a tool that errors leaves the call that asked for it
|
|
172
|
+
// reporting `ok`. Bounded, and says so via `truncated`. Empty when the sink is not wired /
|
|
173
|
+
// storing is off. The BROWSE read: fetched when the trajectory is opened, since it carries
|
|
174
|
+
// every argument and result the run captured.
|
|
175
|
+
getToolCalls: (workspaceId: string, executionId: string) =>
|
|
176
|
+
send(getExecutionToolCallsContract, {
|
|
177
|
+
pathPrefix: ws(workspaceId),
|
|
178
|
+
pathParams: { executionId },
|
|
179
|
+
}),
|
|
180
|
+
|
|
181
|
+
// The run's failing tool calls plus its exact `{ total, failed }`, counted in SQL rather
|
|
182
|
+
// than off any list. The panel's headline read, made on open: cheap enough to front the
|
|
183
|
+
// page, and exact enough that it never disagrees with the debug overview on a long run.
|
|
184
|
+
getToolCallFailures: (workspaceId: string, executionId: string) =>
|
|
185
|
+
send(getExecutionToolCallFailuresContract, {
|
|
186
|
+
pathPrefix: ws(workspaceId),
|
|
187
|
+
pathParams: { executionId },
|
|
188
|
+
}),
|
|
189
|
+
|
|
168
190
|
// ---- spend safeguard --------------------------------------------------
|
|
169
191
|
resumeSpend: (workspaceId: string) =>
|
|
170
192
|
send(resumeSpendContract, { pathPrefix: ws(workspaceId) }),
|