feat: dispatch newly installed experts in the background without /new (#420)
* refactor: rename AGENTS.md to EXPERT.md per EvoSkills convention * feat: resolve newly installed experts on start_async_task miss * chore: reword the /expert invite hint to state the dispatch boundary * docs: note the resolve-on-miss caller in build_expert_async_subagent_specs * fix: thread the construction cfg through resolve-on-miss and symmetric setdefault * fix: guard agent_map iteration against concurrent resolve-on-miss writes * fix: warn once per broken expert on repeated resolve-on-miss walks * fix: scope the /expert invite hint to newly installed experts * docs: note live uninstalls as a resolve-on-miss limitation * chore: isolate the warn-once collision test key from the route-specs suite
This commit is contained in:
@@ -575,10 +575,14 @@ def _route_async_specs_through_evo_middleware(
|
||||
route all async dispatch through our subclass, we strip AsyncSubAgent
|
||||
specs from ``subs`` here and hand them to our middleware.
|
||||
|
||||
Also folds in ``AsyncSubAgent`` specs for installed
|
||||
installed expert skills — all pointing at the shared
|
||||
``expert-container-async`` graph, marked ``is_expert=True`` so the
|
||||
middleware requires a payload with ``skill_name``.
|
||||
Also folds in ``AsyncSubAgent`` specs for installed expert skills —
|
||||
all pointing at the shared ``expert-container-async`` graph, marked
|
||||
``is_expert=True`` so the middleware requires a payload with
|
||||
``skill_name``.
|
||||
|
||||
The completion watcher (``AsyncWatcherMiddleware``) is found or created
|
||||
before the middleware so the middleware's resolve-on-miss start tool can
|
||||
hold the watcher's agent dict by reference — see the wiring block below.
|
||||
|
||||
Returns:
|
||||
``subs`` with ``graph_id``-carrying entries removed. Safe to pass
|
||||
@@ -596,6 +600,71 @@ def _route_async_specs_through_evo_middleware(
|
||||
expert_specs = build_expert_async_subagent_specs(cfg=cfg)
|
||||
async_specs.extend(expert_specs)
|
||||
|
||||
# Find or create the completion watcher BEFORE constructing
|
||||
# ``EvoAsyncSubAgentMiddleware``: the middleware's resolve-on-miss start
|
||||
# tool must hold the watcher's agent dict by reference, so an expert
|
||||
# discovered mid-session lands in the dispatch table and the watcher in
|
||||
# one step. Without the watcher update, dispatch succeeds but the
|
||||
# watcher's ``get_async(agent_name)`` raises KeyError inside its
|
||||
# ``try/except`` and the completion notification silently never fires.
|
||||
watcher_agents: dict | None = None
|
||||
watcher = next(
|
||||
(m for m in base_middleware if isinstance(m, AsyncWatcherMiddleware)),
|
||||
None,
|
||||
)
|
||||
if watcher is None:
|
||||
# No YAML async subagents were registered, so ``_maybe_swap`` did
|
||||
# not install the watcher. Install it now so experts still get
|
||||
# completion notifications. Appended before the middleware's
|
||||
# index-0 insert below, which yields the same final order as
|
||||
# append-after-insert: ``[EvoAsync..., ..., watcher]``.
|
||||
if expert_specs:
|
||||
from .cli import async_notifier
|
||||
|
||||
watcher = AsyncWatcherMiddleware(
|
||||
{s["name"]: s for s in expert_specs},
|
||||
notifier=async_notifier,
|
||||
)
|
||||
base_middleware.append(watcher)
|
||||
elif expert_specs:
|
||||
# Extend AsyncWatcherMiddleware's client cache with expert specs so
|
||||
# start_async_task launches for experts spawn a completion watcher —
|
||||
# otherwise the watcher's ``get_async(agent_name)`` KeyErrors on the
|
||||
# expert name, no notification is enqueued, and the main agent never
|
||||
# learns the task finished. ``_maybe_swap_async_subagents`` above only
|
||||
# populates the watcher with YAML-defined async subagents
|
||||
# (writing-agent, data-analysis-agent, scheduler); this hook folds in
|
||||
# the experts too.
|
||||
#
|
||||
# The mutation reaches through two layers of private state:
|
||||
# ``AsyncWatcherMiddleware._clients`` (our own) and
|
||||
# ``_ClientCache._agents`` (upstream deepagents). If upstream ever
|
||||
# renames ``_agents`` or wraps it in an immutable snapshot, the
|
||||
# ``.update(...)`` below silently lands on nothing — expert
|
||||
# completion nudges then stop firing without a diagnostic surface.
|
||||
# Convert that silent-drop into a grep-able error line and leave
|
||||
# ``watcher_agents`` unset; expert dispatches still work (without
|
||||
# completion notifications and without mid-session resolution into
|
||||
# the watcher) until upstream drift is fixed.
|
||||
if not hasattr(watcher._clients, "_agents"):
|
||||
logging.getLogger(__name__).error(
|
||||
"AsyncWatcherMiddleware._clients has no `_agents` slot — "
|
||||
"deepagents internal renamed; expert completion "
|
||||
"notifications will not fire until the extension hook is "
|
||||
"updated to the new attribute name."
|
||||
)
|
||||
watcher = None
|
||||
else:
|
||||
watcher._clients._agents.update({s["name"]: s for s in expert_specs})
|
||||
# The second ``hasattr`` is not redundant with the one in the ``elif``
|
||||
# above: that check only runs when ``expert_specs`` is non-empty. When a
|
||||
# pre-existing watcher has an empty expert set, this is the only guard
|
||||
# standing between an upstream rename of ``_ClientCache._agents`` and an
|
||||
# AttributeError that would kill agent construction — without it, the
|
||||
# drift degrades to "no completion nudges" instead of crashing.
|
||||
if watcher is not None and hasattr(watcher._clients, "_agents"):
|
||||
watcher_agents = watcher._clients._agents
|
||||
|
||||
if async_specs:
|
||||
# ``_maybe_swap_async_subagents`` installs the model-passthrough patch
|
||||
# only when the yaml-async spec list is non-empty. An expert-only setup
|
||||
@@ -612,52 +681,16 @@ def _route_async_specs_through_evo_middleware(
|
||||
# volatile memory tail, invalidating the cached prefix on every
|
||||
# memory change.
|
||||
base_middleware.insert(
|
||||
0, EvoAsyncSubAgentMiddleware(async_subagents=async_specs)
|
||||
0,
|
||||
EvoAsyncSubAgentMiddleware(
|
||||
async_subagents=async_specs,
|
||||
watcher_agents=watcher_agents,
|
||||
# The construction cfg, so resolve-on-miss specs the same
|
||||
# langgraph_dev_port the construction-time specs used instead
|
||||
# of re-reading config from disk at dispatch time.
|
||||
cfg=cfg,
|
||||
),
|
||||
)
|
||||
|
||||
# Extend AsyncWatcherMiddleware's client cache with expert specs so
|
||||
# start_async_task launches for experts spawn a completion watcher —
|
||||
# otherwise the watcher's ``get_async(agent_name)`` KeyErrors on the
|
||||
# expert name, no notification is enqueued, and the main agent never
|
||||
# learns the task finished. ``_maybe_swap_async_subagents`` above only
|
||||
# populates the watcher with YAML-defined async subagents (writing-agent,
|
||||
# data-analysis-agent, scheduler); this hook folds in the experts too.
|
||||
if expert_specs:
|
||||
watcher = next(
|
||||
(m for m in base_middleware if isinstance(m, AsyncWatcherMiddleware)),
|
||||
None,
|
||||
)
|
||||
if watcher is not None:
|
||||
# The mutation reaches through two layers of private state:
|
||||
# ``AsyncWatcherMiddleware._clients`` (our own) and
|
||||
# ``_ClientCache._agents`` (upstream deepagents). If upstream ever
|
||||
# renames ``_agents`` or wraps it in an immutable snapshot, the
|
||||
# ``.update(...)`` below silently lands on nothing — expert
|
||||
# completion nudges then stop firing without a diagnostic surface.
|
||||
# Convert that silent-drop into a grep-able error line and bail
|
||||
# out of the extension path; expert dispatches still work, just
|
||||
# without completion notifications until upstream drift is fixed.
|
||||
if not hasattr(watcher._clients, "_agents"):
|
||||
logging.getLogger(__name__).error(
|
||||
"AsyncWatcherMiddleware._clients has no `_agents` slot — "
|
||||
"deepagents internal renamed; expert completion "
|
||||
"notifications will not fire until the extension hook is "
|
||||
"updated to the new attribute name."
|
||||
)
|
||||
return sync_subs
|
||||
watcher._clients._agents.update({s["name"]: s for s in expert_specs})
|
||||
else:
|
||||
# No YAML async subagents were registered, so ``_maybe_swap`` did
|
||||
# not install the watcher. Install it now so experts still get
|
||||
# completion notifications.
|
||||
from .cli import async_notifier
|
||||
|
||||
base_middleware.append(
|
||||
AsyncWatcherMiddleware(
|
||||
{s["name"]: s for s in expert_specs},
|
||||
notifier=async_notifier,
|
||||
)
|
||||
)
|
||||
return sync_subs
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user