Skip to content

fix: wrong contacts on tasks, and orchestrators that never delegate on cloud - #100

Merged
vdittgen merged 6 commits into
mainfrom
fix/entity-precision
Aug 5, 2026
Merged

vdittgen merged 6 commits into
mainfrom
fix/entity-precision

Conversation

@vdittgen

@vdittgen vdittgen commented Aug 4, 2026 •

Copy link
Copy Markdown
Owner

Six commits, reviewable independently.

1. Contact names matched as bare substrings

extract_entities tested name_variant in combined, so "ana" matched inside Mariana, Susana, analysis and banana. Demonstrated against the old code:

"Can you send Mariana the deck?"        -> [('ana','p-ana'), ('mariana','p-mariana')]
"Finish the analysis and buy a banana"  -> [('ana','p-ana')]

Both contacts attached to a message naming only one — the "wrong contact on a task" report.

_build_name_index compounded it: every name part >2 chars was indexed with setdefault, so when two contacts share a first name the first-indexed one permanently owned that token.

Now: whole-word matching anchored with (?<!\w)/(?!\w) (so O'Brien and Ana-Paula still work, and accented names get real boundaries), longest-variant-first, and an index keeping only single-owner variants — a token two people share resolves to nobody rather than to whoever came first.

2. Orchestrators never delegate under native structured output

Pro builds force native structured output for cloud models (overlay patch 08), whose comment claims auto tool calls still work. Measured against DeepInfra, same prompt and tools, only that field changed:

mode delegations
tool ['goal_extractor']
native [] — every time

So on cloud task_curator never reaches task_proposer, and no tasks are produced.

Fixed per agent rather than per endpoint: SBOrchestrator / SBDeepAgent carry delegates = True and get ToolOutput; every other agent keeps the profile default, so the native path patch 08 needs is untouched. No change to ModelFactory or its cache.

Wants one live check through gateway.arandu.life before shipping — tool-mode working there is the premise patch 08 doubted. Verified against DeepInfra directly: all three probes now route correctly.

3. Eval suites for task_proposer / task_curator

Neither agent had coverage, so model changes couldn't be evaluated. Also fixes two harness bugs: cases run concurrently (the curator adapter now uses a ContextVar instead of racing on registry swaps), and ContainsIds stringified list-valued keys so ["m1"] never matched "m1".

These suites were then used to score seven candidate models for arandu-cloud#32.

4. Delete the unused insight and weekly_digest agents

Neither SBAgent had a production caller — both were instantiated only by the eval adapter, so every logged run was the eval suite and no user ever saw the output.

  • InsightAgent: InsightGenerator (which does fill the Dashboard) authors prose via BrainAgentV2.ask() at three sites and never touched the agent. The generator, _insights and the Dashboard cards are untouched — only the orphaned wrapper is gone.
  • WeeklyDigestAgent: its docstring claimed the legacy extension delegates to it; the extension calls context.ask_llm() directly. That extension is removed too — it has never run (no ext_weekly_digest_summaries table) and is not installed.

5. Skip an eval suite when nothing that affects it changed

A full pass is 213 cases / 480 LLM calls, billed to the user's real balance when the provider is arandu_cloud — **$0.16 a run**, measured over six batch days. Re-running an unchanged suite reproduces a known answer.

run_suite now hashes the model, the resolved system prompt and the dataset bytes, and skips when that fingerprint has already passed.

scenario result
unchanged skipped
model / prompt / dataset changed runs
reverted (A→B→A) skipped
previous run failed runs
fingerprint unresolvable runs

--force overrides. The fingerprint does not cover agent source — use --force after editing agent code.

Two supporting fixes: CLI runs are now recorded in agent_eval_runs with trigger='cli' (only the Agents-page button used to write there, so make evals left no trace and eval spend was unattributable), and suite_fingerprint bootstraps the registry once per process — a bare python -m evals.run_evals registers no agents, so every fingerprint came back None and the gate never fired. Guarded ALTER TABLE ... ADD COLUMN fingerprint for existing installs.

The README's "~$0.01 per full run" is corrected.

6. lru_cache latch for the registry bootstrap

Addresses the CodeQL unused-global-variable alert on _REGISTRY_READY. lru_cache(maxsize=1) states "once per process" more directly and also caches the failure path, which is the behaviour we want.

On the two remaining CodeQL alerts

Both flag evals/run_evals.py importing src.agents.eval_runner as beginning an import cycle. The cycle is real but pre-existing — eval_runner.py has imported evals.run_evals at lines 490 and 527 since before this PR. Both directions use function-level imports, which is precisely the standard mitigation: nothing cycles at import time.

The suggested autofix (importlib.import_module("src.agents.eval_runner")) does not remove the cycle — it is the same import, hidden from static analysis, at the cost of type checking and IDE navigation. I have not applied it. Untangling the eval_runner ↔ evals.run_evals dependency properly means relocating AGENT_SUITE_MAP, which belongs in its own PR.

Testing

2,967 passed. The 9 failures are pre-existing and fail identically on a clean tree (5 WhatsApp bridge, 2 sync lifecycle, 2 worker reindex).

🤖 Generated with Claude Code

vdittgen and others added 5 commits August 4, 2026 09:11
`extract_entities` tested `name_variant in combined`, so a short name
matched inside any longer word: "ana" hit *Mariana*, *Susana*,
*analysis* and *banana*. A message mentioning only Mariana resolved to
BOTH Ana and Mariana, and whichever node id landed first won the
mention — that is the "wrong contact attached to a task" report.

`_build_name_index` compounded it. Every name part >2 chars was
indexed with `setdefault`, so when two contacts share a first name the
one indexed first permanently owned that token and every later mention
of the other person mis-resolved to them.

Three changes:

- New `src/core/name_matching.py` holds the rule in one place.
  Boundaries use `(?<!\w)`/`(?!\w)` rather than `\b` so names that
  begin or end with punctuation (O'Brien, Ana-Paula) still anchor, and
  matching is Unicode-aware so accented names get real boundaries.
  Patterns are lru_cached — the same contacts are rematched on every
  query.
- Matching is longest-variant-first, so a full name claims a mention
  ahead of a bare first name that overlaps it.
- The index now accumulates `variant -> {owner ids}` and keeps only
  single-owner variants. A token two people share resolves to nobody
  instead of to whoever was indexed first: callers can degrade from a
  missing contact, but cannot detect a confidently wrong one.

`question_patterns.extract_entities` had the same substring test and
now shares the helper.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Pro builds switch cloud models to native structured output (overlay
patch 08) because the gateway honours `response_format` but ignores
`tool_choice="required"`. That patch's comment claims regular auto tool
calls still work. For agents that delegate, they do not.

Measured against DeepInfra, same prompt, same tools, only the profile
field changed:

    default_structured_output_mode="tool"    -> ['goal_extractor']
    default_structured_output_mode="native"  -> []   (every time)

The orchestrator answers directly and never calls a `delegate_*` tool,
so on cloud `task_curator` never reaches `goal_extractor` /
`task_proposer` and no goals or tasks are ever produced. Consistent
with `task_proposer` being absent from `agent_run_log` entirely while
`goal_extractor` shows 133 runs.

Fix scopes the mode per agent instead of per endpoint: `SBOrchestrator`
and `SBDeepAgent` carry `delegates = True`, and `_output_spec_for()`
wraps their `output_type` in `pydantic_ai.ToolOutput`. Every other
agent keeps the model profile's default, so the native path that patch
08 was added for is untouched.

It lands at the single `Agent(...)` construction site in
`_get_pa_agent`, so `ModelFactory` and its endpoint cache — which is
what makes bearer rotation work — are not touched.

Verified with the native profile forced: the three routing probes that
returned `[]` now delegate to `goal_extractor`, `task_proposer` and
`daily_scheduler` respectively. Still worth one live call through
gateway.arandu.life before shipping, since tool-mode against the real
gateway is the premise patch 08 doubted.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The two agents behind the "goals and tasks extraction is wrong"
reports had no eval coverage, so there was no way to tell whether a
model swap helped.

`task_proposer` (7 cases) covers the judgement calls that actually go
wrong: task vs pending reply, and who owes what. Cases carry near-miss
names (Ana/Mariana, Bruno/Brunela) so a model that pattern-matches on
a name prefix fails.

`task_curator` (9 cases) grades routing only. The five children are
replaced with stubs, so a case costs one curator call instead of
fanning out into a real goal-extractor or scheduler run. `DelegatedTo`
requires an exact match, so over-delegating fails too — every extra
hop is a paid call the user waits on.

Two harness fixes found while building these:

- `Dataset.evaluate_sync` runs cases concurrently, so the curator
  adapter installs its stubs once and collects delegations through a
  ContextVar. Swapping registry entries around each case raced: one
  case restored the real definitions while another was mid-run, and
  every row scored "delegated to nobody".
- `ContainsIds` stringified list-valued keys, so a
  `source_message_ids` of `["m1"]` became `"['m1']"` and never matched
  the expected `"m1"` — four grounding assertions failed for every
  model regardless of output. List values are now flattened; scalar
  keys are unaffected.

The adapters also register their agent family explicitly, since the
registry is empty in a bare eval process.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Neither SBAgent had a production caller — both were instantiated only
by the eval adapter, so every logged run was the eval suite and no user
ever saw their output.

- ``InsightAgent``: ``InsightGenerator`` (which does fill the Dashboard)
  authors prose through ``BrainAgentV2.ask()`` at three sites and never
  touched the agent. The generator, its ``_insights`` table and the
  Dashboard cards are untouched — only the orphaned wrapper is gone.
- ``WeeklyDigestAgent``: its docstring claimed the legacy extension
  "delegates to" it; the extension actually calls ``context.ask_llm()``
  directly. That extension is removed too — it has never run on this
  install (no ``ext_weekly_digest_summaries`` table) and is not
  installed as a connector.

Removes both agents, the weekly-digest extension, their registrations
and Brain delegation entry, the ``insight``/``weekly_digest`` eval
suites and adapters, the ``InsightDraft``/``DigestSummary`` output
types, the frozen ``weekly_digest_v1`` prompt and its golden fixture.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
A full pass is 213 cases across 29 suites, ~480 LLM calls once the
injection-firewall companions are counted, and it bills the user's real
cloud balance when the provider is arandu_cloud — about $0.16 a run,
measured. Re-running an unchanged suite only reproduces a known answer,
and the suites hit exactly the agents now on the premium tier.

``run_suite`` now hashes the three inputs that can change a result —
the model serving the agent, its resolved system prompt, and the
dataset bytes — and skips when that fingerprint has already passed.

Design notes:

- Only ``status='ok'`` suppresses. A failed run always re-runs, because
  confirming whether the failure persists is the point.
- Matching is "has this fingerprint EVER passed", not "was it the most
  recent". An A -> B -> A edit then skips on the return to A, which is
  the case we can be surest about.
- An unresolvable input yields ``None``, which means "cannot prove
  unchanged" and the suite runs. The gate can only ever save a run it
  is certain about.
- ``--force`` overrides. The fingerprint does not cover agent *source*,
  so use it after editing agent code.

Two supporting fixes:

- CLI runs are now recorded in ``agent_eval_runs`` with
  ``trigger='cli'``. Only the Agents-page button used to write there,
  so a ``make evals`` left no trace and eval spend was unattributable —
  which is why this cost had to be reverse-engineered from the run log.
- ``suite_fingerprint`` bootstraps the agent registry once per process.
  A bare ``python -m evals.run_evals`` imports nothing that registers
  agents, so every fingerprint came back ``None`` and the gate never
  fired.

Adds a guarded ``fingerprint`` column; installs predating this migrate
in place. The README's "~$0.01 per full run" claim is corrected — it
was off by more than an order of magnitude.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Comment thread evals/run_evals.py Dismissed
Comment thread evals/run_evals.py Dismissed
Comment thread src/agents/eval_runner.py Fixed
Replaces the `_REGISTRY_READY` module global CodeQL flagged. The
`global` + read + rebind idiom is exactly the shape its
unused-global-variable query mishandles, and `lru_cache(maxsize=1)`
says "run once per process" more directly anyway.

It also caches the failure path, which is the behaviour we want: a
registry that could not be built will not build on the next call, and
retrying per suite would re-pay the import cost to reach the same
`None`.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@vdittgen
vdittgen merged commit 9c7c54f into main Aug 5, 2026
5 of 6 checks passed
@vdittgen
vdittgen deleted the fix/entity-precision branch August 5, 2026 22:43
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants