Skip to content

Commit e984494

Browse files
committed
✨ feat(api,graph,console,core): search runs surface the work — coordinator trace lane, agent-emitted result cards, honest run stats; async workspace indexing; external session cwd (#100)
Root-caused from live run `run-02ad562781` (`/search?run=run-02ad562781&ws=ws-00f72e0c`): a fast-tier orchestrated run completed with an accurate synthesis but a blank instrument panel — `trace: []`, `results: []`, `confidence 0.0`, `sources_count 0`, `total_ms 0` (real elapsed ~170s, 33 tool calls). Mongo + Langfuse confirmed the root `scg-search` agent inlined ALL the work and never spawned a probe, and the entire event-log projection keyed off `sub_agent` events alone. This PR makes the run surface honest end-to-end and folds in the three companion regressions found during the same sweep. All wire changes are additive (no `OUTPUT_CONTRACT_VERSION` bump). ## New features - **Coordinator trace lane** (#95-A): root-agent tool activity projects onto the run event log live (`RunEventStreamer`) and at settle through ONE shared `CoordinatorTrace` projection — zero-probe runs render a real trace. Lines are digests only (tool id + capped input hint, `ok|error`); the tool's result payload is never read (connector returns can carry secrets). - **Agent-emitted result cards** (#95-B): new `scg_results` SessionTool in `mewbo_graph.plugins.scg` — transcript-as-transport: the tool only validates (≤50 entries, `extra="forbid"`, relevance/confidence 0..1) and the api projects entries → `result` events + `RunPayload.results` (stable `r-<run8>-<n>` ids = live↔settle dedup key). The `scg-search` playbook now emits the cards before the final synthesis (root-only). - **Honest settle metrics** (#95-C): `total_ms` computed `started_at→settle` on `run_done`, the payload, and the fail path; `confidence`/`sources_count` cover root-inline runs (data-bearing probe lanes ∪ emitted results' distinct sources) with documented provenance; unknown still suppresses, never fabricates. - **Results-page top band rebuilt** (#96): the query lives ONLY in the composer (the duplicated italic-mono echo is gone); `RunStats` joins only earned parts — a finished run never shows `0.0s` or `0 results` (snapshot elapsed falls back `total_ms` → `created_at→completed_at`); Copy-link + Configure sit in a calm right-aligned meta row; coordinator lanes render a `Workflow` glyph across ProgressStrip/RightRail/TraceDrawer, never a blank avatar or a misleading `0` chip. - **External session cwd** (#91): `POST /api/sessions` + `/query` accept an optional `cwd` behind `api.allow_external_cwd` (default OFF) via one `ExternalCwdPolicy` seam — explicit cwd wins over project resolution, persists in session context, and is honored on `/message` re-engagement and the diff endpoints. Lets external workspace managers (Grove) anchor sessions in their own worktrees without provided-path v_projects (which the reaper deletes). ## Stability - **Workspace create/edit is register-and-return** (#97, serious UX regression): the auto-map fan-out (live MCP descriptor builds + drift checks) ran in the Flask request thread — "Create Workspace" blocked for N MCP handshakes and read as browser-dependent indexing. Step 1 (virtual-config refresh) stays sync; everything else runs on a named daemon worker. `join_last_fan_out` gives route tests deterministic settling. Map jobs were already durable/background. - **Recents dedupe + unique cmdk identity** (#98-A): duplicate past queries no longer hover/select together — `CommandItem value` was the bare query text (cmdk identity collision); now `pastQueryKey` (run_id) + `dedupePastQueries` (normalized text, most-recent wins) shared by SearchBar + LandingPanel. - **Workspace card polish** (#98-B): single-line meta shelf by contract (`mt-auto flex-nowrap`, `flex-none` action cluster + "N past" pill, `min-w-0 overflow-hidden` avatar rail), footer pinned so rows in the grid read consistently. ## Docs - CLAUDE.md refresh across `agentic_search` (api + scg + console), the scg plugin suite, and `apps/mewbo_api`: coordinator-lane/`scg_results`/metrics decisions, the open `resolve_entity`-in-search gap (#95-D — gating entity tools on wiki-ctx presence is the future fix, not a manifest hack), RunStats honesty rules, the cmdk value-collision trap, the async fan-out invariant, and the #91 endpoint contract. ## Validation - Python: ruff clean, mypy clean (230 files), **4373 passed / 3 skipped** (includes 13 new `scg_results` tests, 12 new `external_cwd` tests, 2 new async fan-out tests, reenrich suite adapted to the async seam). - Console: vitest **342 passed / 4 skipped** (full suite; +14 new across ResultsPanel/SearchBar/LandingPanel), `tsc --noEmit` clean, eslint clean. - Caveat: validated locally + by test suites; not yet re-verified against the deployed stack (api/graph are bind-mounted — `docker compose up -d`; console needs `npm run build` + restart). Closes #95. Closes #96. Closes #97. Closes #98. Closes #91. Reviewed-on: https://git.hurricane.home/bearlike/Assistant/pulls/100
1 parent c786f5f commit e984494

29 files changed

Lines changed: 2751 additions & 225 deletions

apps/mewbo_api/CLAUDE.md

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,17 @@ Scope: this file applies to the `apps/mewbo_api/` package. It captures runtime b
1414
## Runtime flow (what actually happens)
1515
- Entry point: `apps/mewbo_api/src/mewbo_api/backend.py` (HTTP API framework).
1616
- Session endpoints:
17-
- `POST /api/sessions` create session
17+
- `POST /api/sessions` create session — accepts an optional external `cwd`
18+
(top-level or `context.cwd`; also on `POST .../query`) gated behind
19+
`api.allow_external_cwd` (default OFF). `ExternalCwdPolicy` (backend.py) is
20+
the one seam: flag off + cwd present → structured 403; flag on → must be an
21+
existing directory (else 400); explicit cwd WINS over project-derived and is
22+
persisted as `context_payload["cwd"]` so `/message` re-engagement and
23+
`_resolve_session_cwd` (diff endpoints) keep resolving it (#91 — external
24+
workspace managers like Grove anchor sessions in their own worktrees;
25+
registering provided-path v_projects is NOT a substitute: the reaper
26+
permanently deletes childless provided-path parents). Docker rule applies:
27+
the path must be visible in the api container at the identical path.
1828
- `GET /api/sessions` list sessions (each summary carries `origin``user|wiki|search|channel` provenance computed in core `summarize_session`, forwarded verbatim; the console badges/filters on it)
1929
- `POST /api/sessions/{session_id}/query` enqueue run or core command
2030
- `GET /api/sessions/{session_id}/events?after=...` poll events

apps/mewbo_api/src/mewbo_api/agentic_search/CLAUDE.md

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -134,6 +134,11 @@ reducer switches on). Rules:
134134
`result` events drive arrival order. The prototype's `finish_delay_ms`
135135
/ `t_ms` are deprecated decorative fields — real ordering comes from
136136
event arrival, never a client timer.
137+
- Orchestrated runs now populate ALL of it (#95): `agent_*` includes a
138+
synthetic **coordinator lane** for root-inlined tool activity, `result`
139+
events come from the agent's `scg_results` emission, and
140+
`total_ms`/`confidence`/`sources_count` are computed, never defaulted —
141+
see `scg/CLAUDE.md` → "Coordinator lane + result cards + honest settle".
137142

138143
## `store.py` is the substitution boundary
139144

@@ -203,7 +208,15 @@ behavior). `WorkspaceSourceSync.on_workspace_saved` (`source_sync.py`) is the
203208
POST/PATCH hook: it refreshes the virtual config, then auto-maps newly-enabled
204209
**live** sources (idempotent — skips already-mapped/in-flight; a terminal/failed
205210
job does NOT block a re-map, so a previously-unreachable source re-maps once its
206-
URL is fixed). It ALSO re-maps already-mapped enabled sources whose live tool
211+
URL is fixed). **The hook is register-and-return (#97):** only step 1 (virtual
212+
config refresh + NL fingerprint) runs on the request thread; the whole auto-map
213+
fan-out (mappable/drifted/re-enrich resolution + live descriptor builds +
214+
`MapSourceJob.start`) runs on one named daemon thread
215+
(`workspace-automap-<id>`) — the descriptor build is a live MCP handshake per
216+
source, and doing it in-request blocked "Create Workspace" for N handshakes
217+
(the user read it as browser-dependent indexing). The method returns the
218+
`Thread | None` so tests join deterministically; routes ignore it. Never move
219+
the fan-out back in-request, and never let the thread body raise. It ALSO re-maps already-mapped enabled sources whose live tool
207220
list drifted from the stamped `ManifestHash` (#81-C), and carries the workspace
208221
`instructions`/`desc` as untrusted `nl_context` to seed the map-time enrich step
209222
(#81-B — see scg/CLAUDE.md). **Workspace editing IS a graph-lifecycle event

apps/mewbo_api/src/mewbo_api/agentic_search/scg/CLAUDE.md

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,53 @@ now echoed as an additive `summary` on that event (core `spawn_agent`). So:
8888
evidence panel is empty (the same "mirror the real engine shape" rule as the
8989
`completion` payload above).
9090

91+
## Coordinator lane + result cards + honest settle (#95)
92+
93+
A fast-tier run can legitimately spawn ZERO probes (the root inlines every
94+
tool call — verified live, run `run-02ad562781`), and the original projection
95+
keyed everything off `sub_agent` events → blank trace, `results=[]`,
96+
`total_ms=0` beside a correct synthesis. The fixes, all additive on the wire
97+
(no `OUTPUT_CONTRACT_VERSION` bump):
98+
99+
- **Coordinator lane** — root `tool_result` events project into ONE synthetic
100+
lane (id `coordinator`, name `scg-search`, `source_id ""`).
101+
`CoordinatorTrace` (run_streamer.py) is the pure shared projection (the
102+
`ProbeTrace` stance: live `_project` and settle render byte-identically).
103+
SECURITY: a line is a digest only — tool_id + scalar-input hint capped at
104+
120 chars; the tool's RESULT payload is never read (connector returns can
105+
carry secrets/PII). The `scg_results` call renders as `emitted N results`,
106+
never its entries. The lane has no `stop` lifecycle — its `agent_done`
107+
fires at settle, `empty = no data-bearing probe AND no results`. Slots come
108+
from merged first-seen order (`_assign_lane_slots`) so settle reproduces
109+
the live interleaving. Probe `tool_result`s never appear here — probes run
110+
in their own sessions; this session's bus only carries the root's.
111+
- **`scg_results` = transcript-as-transport.** The tool
112+
(`mewbo_graph.plugins.scg.results`, granted via `TRAVERSAL_TOOLS`)
113+
validates (≤50 entries, `extra="forbid"`, relevance/confidence 0..1) and
114+
returns `{ok, count}` — it writes NOTHING (layering: the library never
115+
touches the api run store; no MapPhaseSink-style DI needed because the
116+
transcript already reaches the api). `ResultsProjection` maps entries →
117+
`SearchResult` with stable ids `r-<run_id8>-<n>` — that id is the
118+
live↔settle dedup key. Entry `confidence` folds into `relevance` only when
119+
`relevance` is absent (no wire field for it). The playbook's terminal
120+
discipline: the ROOT emits once, before synthesis; probes never call it.
121+
- **Metrics provenance** (`_synthesis_metrics`): `sources_count` = distinct
122+
grounding sources = data-bearing probe lanes (keyed by `agent_id` — the
123+
wire `source_id` is the shared parent grouping key, useless for
124+
distinctness) ∪ emitted results' `source` fields. `confidence` =
125+
data-bearing/probes when probes ran, else mean folded entry score, else
126+
`(0.0, 0)` (console suppresses). The coordinator lane is NEVER a probe.
127+
`total_ms` = `started_at→settle` wall clock, stamped on `run_done`,
128+
`RunPayload`, and the `_fail` payload — never hardcoded 0 again.
129+
- **`resolve_entity` trap (open, #95-D):** it reaches search-run sessions via
130+
the scg plugin manifest + #84's capability-driven `build_for`, but a search
131+
RUN never satisfies `resolve_qa_ctx` (no QA answer, no
132+
`structured_workspace` event) → it always errors `wiki ctx not found`.
133+
Exclusion is NOT one-seam: the same registration serves
134+
`scg-search-structured`, where the ctx DOES resolve. The clean future fix
135+
is gating entity tools on wiki-ctx presence rather than the bare `scg`
136+
capability — don't hack the manifest.
137+
91138
## Two correctness traps (both silent — no exception)
92139

93140
1. **Two different `StructureProvider` protocols share a name root and nothing

apps/mewbo_api/src/mewbo_api/agentic_search/scg/orchestrated_runner.py

Lines changed: 182 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@
4646
from __future__ import annotations
4747

4848
import threading
49+
from datetime import datetime
4950
from typing import Any, Literal
5051

5152
from mewbo_core.common import get_logger
@@ -61,12 +62,18 @@
6162
RunRecord,
6263
SearchResult,
6364
TraceAgent,
65+
TraceLine,
6466
Workspace,
6567
utc_now_iso,
6668
)
6769
from .config import ScgConfig
6870
from .playbooks import load_playbook
69-
from .run_streamer import ProbeTrace, RunEventStreamer
71+
from .run_streamer import (
72+
CoordinatorTrace,
73+
ProbeTrace,
74+
ResultsProjection,
75+
RunEventStreamer,
76+
)
7077
from .workspace_binding import WorkspaceGraphBinding
7178

7279
logging = get_logger(name="api.agentic_search.scg.orchestrated_runner")
@@ -299,8 +306,29 @@ def _settle(
299306
return None
300307

301308
trace = self._build_trace(records)
309+
# The root coordinator's own tool activity (#95): a root-inline run (fast
310+
# tier, no probe sub-agents) streams nothing through the probe lanes, so
311+
# the root's ``tool_result`` events are projected as one extra lane. Its
312+
# slot is its first-seen ordinal across the MERGED stream (probes +
313+
# coordinator) so the settle reconcile honours transcript order exactly as
314+
# the live path does (coordinator opens when its first tool_result lands).
315+
# This re-slots the probe lanes in place to leave room for it.
316+
coordinator_slot = self._assign_lane_slots(records, trace)
317+
coordinator_lines = self._build_coordinator_lines(records)
318+
# ``scg_results`` emits the discrete result cards (transcript-as-transport):
319+
# the root calls it once before synthesis; the api projects its entries.
320+
results = self._build_results(run.run_id, records)
321+
# A run is data-bearing iff any probe returned data OR any result emitted.
322+
with_data_probes = [a for a in trace if not ProbeTrace.is_dead_end(a.result)]
323+
has_data = bool(with_data_probes) or bool(results)
324+
302325
if streamer is not None:
303326
streamer.reconcile_missing(trace)
327+
streamer.reconcile_results(results)
328+
if coordinator_lines or streamer.coordinator_opened():
329+
streamer.reconcile_coordinator(
330+
coordinator_lines, slot=coordinator_slot, has_data=has_data
331+
)
304332
else:
305333
# No live streamer (defensive / legacy call): emit the full trace.
306334
for agent in trace:
@@ -323,19 +351,16 @@ def _settle(
323351
agent_id=agent.agent_id, results_count=0, empty=not agent.lines
324352
),
325353
)
326-
327-
# Results: the SCG search synthesizes a cited answer rather than emitting
328-
# per-source result cards (the connector return is the verifier, not a
329-
# normalized hit list); ``results`` stays empty until a probe contract
330-
# carries them. The trace + answer are the live surfaces today.
331-
results: list[SearchResult] = []
354+
for item in results:
355+
store.append_run_event(run.run_id, events.result(item=item))
332356

333357
status = self._run_status(summary)
334358
answer_text, err = self._task_result(records)
335-
# Confidence + sources_count come from the live trace's data-bearing
336-
# probes, not a fixture (the echo-era fields read 0% / 0 sources on real
337-
# runs). The trace is the single source of both signals.
338-
confidence, sources_count = self._synthesis_metrics(trace)
359+
# Confidence + sources_count come from REAL probe + result signals, not a
360+
# fixture (the echo-era fields read 0% / 0 sources on real runs). Probes
361+
# AND emitted results both ground the answer (a root-inline run has only
362+
# results); the coordinator lane is NOT a probe — never in the ratio.
363+
confidence, sources_count = self._synthesis_metrics(trace, results)
339364
answer = AnswerSynthesis(
340365
tldr=answer_text, confidence=confidence, sources_count=sources_count
341366
)
@@ -355,8 +380,11 @@ def _settle(
355380
store.append_run_event(run.run_id, events.answer_delta(text=chunk))
356381
store.append_run_event(run.run_id, events.answer_ready(answer=answer))
357382

383+
# Honest elapsed: started_at/created_at ISO → settle, in ms (#95). The
384+
# old hardcoded 0 read ``0ms`` next to a ~3-minute run.
385+
total_ms = self._elapsed_ms(run)
358386
store.append_run_event(
359-
run.run_id, events.run_done(status=status, total_ms=0)
387+
run.run_id, events.run_done(status=status, total_ms=total_ms)
360388
)
361389
payload = RunPayload(
362390
run_id=run.run_id,
@@ -366,7 +394,7 @@ def _settle(
366394
status=status,
367395
tier=run.tier,
368396
model=run.model,
369-
total_ms=0,
397+
total_ms=total_ms,
370398
answer=answer if status == "completed" else AnswerSynthesis(),
371399
results=results,
372400
trace=trace,
@@ -460,21 +488,148 @@ def _build_trace(records: list[dict[str, Any]]) -> list[TraceAgent]:
460488
return [agents[a] for a in order]
461489

462490
@staticmethod
463-
def _synthesis_metrics(trace: list[TraceAgent]) -> tuple[float, int]:
464-
"""Derive ``(confidence, sources_count)`` from the live probe trace.
465-
466-
Honest, defined provenance (see :class:`AnswerSynthesis`): a probe is
467-
*data-bearing* iff its terminal evidence isn't a ``NO DATA`` dead-end.
468-
``sources_count`` = the count of data-bearing probes (each walks one
469-
qualified pathway, so it is the breadth of grounding behind the answer);
470-
``confidence`` = data-bearing / probes run. An empty trace yields
471-
``(0.0, 0)`` so the console suppresses the chip rather than render an
472-
unearned ``0%``.
491+
def _assign_lane_slots(
492+
records: list[dict[str, Any]], trace: list[TraceAgent]
493+
) -> int | None:
494+
"""Re-slot probe lanes by MERGED first-seen order; return coordinator slot.
495+
496+
The live streamer assigns a lane's slot from ``len(self._order)`` at its
497+
first event, so probe and coordinator slots interleave by transcript
498+
arrival. The settle path must reproduce that exact ordering. This scans
499+
the transcript once, assigning each lane (probe ``agent_id`` or the
500+
sentinel coordinator, opened by its first root ``tool_result``) its
501+
first-seen ordinal, then writes the probe ordinals back onto *trace* in
502+
place. Returns the coordinator's ordinal, or ``None`` when the root
503+
issued no tool_result (no coordinator lane).
473504
"""
474-
if not trace:
505+
slots: dict[str, int] = {}
506+
coordinator_key = "__coordinator__"
507+
for rec in records:
508+
kind = rec.get("type")
509+
payload = rec.get("payload") or {}
510+
if kind == "sub_agent":
511+
key = str(payload.get("agent_id") or "")
512+
if not key:
513+
continue
514+
elif kind == "tool_result":
515+
if not CoordinatorTrace.is_root_event(payload):
516+
continue
517+
key = coordinator_key
518+
else:
519+
continue
520+
if key not in slots:
521+
slots[key] = len(slots)
522+
for agent in trace:
523+
if agent.agent_id in slots:
524+
agent.slot = slots[agent.agent_id]
525+
return slots.get(coordinator_key)
526+
527+
@staticmethod
528+
def _build_coordinator_lines(records: list[dict[str, Any]]) -> list[TraceLine]:
529+
"""Project the root's ``tool_result`` events into coordinator trace lines.
530+
531+
The probes run in their OWN sessions, so a ``tool_result`` in THIS
532+
session's transcript is always a ROOT tool call — no agent-id filter is
533+
needed. Each becomes one secret-free :class:`CoordinatorTrace` digest line
534+
(the SAME projection the live streamer uses — DRY), so a reconciled
535+
coordinator lane is byte-identical to one that streamed live. Empty when
536+
the root spawned probes for everything (only ``sub_agent`` events).
537+
"""
538+
lines: list[TraceLine] = []
539+
for rec in records:
540+
if rec.get("type") != "tool_result":
541+
continue
542+
payload = rec.get("payload") or {}
543+
if CoordinatorTrace.is_root_event(payload):
544+
lines.append(CoordinatorTrace.line(payload))
545+
return lines
546+
547+
@staticmethod
548+
def _build_results(
549+
run_id: str, records: list[dict[str, Any]]
550+
) -> list[SearchResult]:
551+
"""Collect the run's result cards from every ``scg_results`` emit.
552+
553+
Transcript-as-transport (#95): the root calls ``scg_results`` once (the
554+
playbook's terminal-discipline step) carrying the discrete cards behind
555+
the answer; the api projects each entry onto a stable-id
556+
:class:`SearchResult` via :class:`ResultsProjection` (the SAME read the
557+
live stream uses, so ids — and thus dedup — agree). Multiple emits are
558+
concatenated in order; the stable id (``r-<run_id8>-<n>`` per emit) keeps
559+
a re-emit from the same call idempotent across live + settle.
560+
"""
561+
out: list[SearchResult] = []
562+
seen: set[str] = set()
563+
for rec in records:
564+
if rec.get("type") != "tool_result":
565+
continue
566+
payload = rec.get("payload") or {}
567+
if not ResultsProjection.is_results_event(payload):
568+
continue
569+
for item in ResultsProjection.parse(run_id, payload):
570+
if item.id in seen:
571+
continue
572+
seen.add(item.id)
573+
out.append(item)
574+
return out
575+
576+
@staticmethod
577+
def _elapsed_ms(run: RunRecord) -> int:
578+
"""Honest run duration: ``started_at``/``created_at`` ISO → now, in ms.
579+
580+
Parses the run's start timestamp (``started_at`` preferred, ``created_at``
581+
fallback) and measures to settle time. ``0`` only when neither parses (a
582+
record with no start stamp) — never the old hardcoded ``0`` that read
583+
``0ms`` beside a multi-minute run.
584+
"""
585+
start_iso = run.started_at or run.created_at
586+
if not start_iso:
587+
return 0
588+
try:
589+
started = datetime.fromisoformat(start_iso)
590+
now = datetime.fromisoformat(utc_now_iso())
591+
except ValueError:
592+
return 0
593+
delta_ms = int((now - started).total_seconds() * 1000)
594+
return max(delta_ms, 0)
595+
596+
@staticmethod
597+
def _synthesis_metrics(
598+
trace: list[TraceAgent], results: list[SearchResult]
599+
) -> tuple[float, int]:
600+
"""Derive ``(confidence, sources_count)`` from probes AND emitted results.
601+
602+
Honest, defined provenance (see :class:`AnswerSynthesis`):
603+
604+
* ``sources_count`` = the count of DISTINCT grounding sources = the union
605+
of (each data-bearing probe lane — keyed by its ``agent_id``, since one
606+
lane is one qualified-pathway walk and its wire ``source_id`` is the
607+
shared parent grouping key, not the connector) ∪ the emitted results'
608+
``source`` fields. So a probe-only run reports its breadth, a
609+
root-inline run reports its distinct connectors, and a mixed run reports
610+
the union.
611+
* ``confidence`` — when probes ran: ``data-bearing probes / probes run``.
612+
When NO probe ran but results were emitted (the root-inline path): the
613+
mean of the emitted entries' folded score (``relevance``, carrying
614+
``confidence`` when no explicit rank), rounded 2dp. Nothing ran AND
615+
nothing emitted → ``(0.0, 0)`` so the console keeps suppressing the
616+
chip. The coordinator lane is NOT a probe — never in the probe ratio.
617+
"""
618+
with_data_probes = [a for a in trace if not ProbeTrace.is_dead_end(a.result)]
619+
sources = {f"probe:{a.agent_id}" for a in with_data_probes}
620+
sources |= {r.source for r in results if r.source}
621+
sources_count = len(sources)
622+
623+
if trace:
624+
confidence = round(len(with_data_probes) / len(trace), 2)
625+
elif results:
626+
# No probes ran (root-inline): mean of the entries' folded score
627+
# (``relevance`` carries ``confidence`` when no explicit rank — see
628+
# ``ResultsProjection._to_result``).
629+
confidence = round(sum(r.relevance for r in results) / len(results), 2)
630+
else:
475631
return 0.0, 0
476-
with_data = [a for a in trace if not ProbeTrace.is_dead_end(a.result)]
477-
return round(len(with_data) / len(trace), 2), len(with_data)
632+
return confidence, sources_count
478633

479634
@staticmethod
480635
def _task_result(
@@ -519,6 +674,7 @@ def _fail(
519674
status="failed",
520675
tier=run.tier,
521676
model=run.model,
677+
total_ms=self._elapsed_ms(run),
522678
error=message,
523679
)
524680
if self._already_settled(store, run.run_id):

0 commit comments

Comments
 (0)