Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
4093c4b
feat(discovery): record parallel tool-call capability and exclude sin…
devin-ai-integration[bot] Aug 31, 2026
b7842be
fix(discovery): preserve parallel tool-call evidence end to end
seonghobae Aug 31, 2026
3a8168c
fix(discovery): suppress Semgrep dynamic-urllib rule on scoped provid…
devin-ai-integration[bot] Aug 31, 2026
74cea16
fix(discovery): harden capability reconciliation
seonghobae Aug 31, 2026
af03bf0
fix(discovery): harden capability reconciliation
seonghobae Aug 31, 2026
8c3021b
Merge remote-tracking branch 'origin/devin/17592943-parallel-tool-cal…
seonghobae Aug 31, 2026
b60401e
fix(discovery): recover capability-blocked runtime agents
seonghobae Aug 31, 2026
fce941d
fix(discovery): avoid sticky blocker transitions
seonghobae Aug 31, 2026
91e8145
fix(discovery): preserve single-tool agent evidence
seonghobae Aug 31, 2026
61607ba
fix(discovery): preserve tool capability provenance
seonghobae Aug 31, 2026
a68fc5c
fix(discovery): reject echoed tool probe definitions
seonghobae Sep 1, 2026
a1103da
Merge remote-tracking branch 'origin/main' into fix/972-continue
claude Sep 1, 2026
bd422bd
fix(972): address Devin review findings on the merge (operator tool-c…
claude Sep 1, 2026
62d59fc
test(discovery): preserve matching operator tool-call overrides
seonghobae Sep 1, 2026
b542003
fix(972): resolve same-polarity tool-call tag ownership collision
claude Sep 1, 2026
424a361
Merge remote-tracking branch 'origin/main' into devin/17592943-parall…
claude Sep 2, 2026
0208f2e
Merge remote-tracking branch 'origin/main' into devin/17592943-parall…
claude Sep 2, 2026
876f167
Merge remote-tracking branch 'origin/main' into devin/17592943-parall…
claude Sep 3, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
100 changes: 95 additions & 5 deletions contextual_orchestrator/__main__.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,13 +23,16 @@
from .kv_config import InMemoryConfigStore
from .model_discovery import (
CONFIGURED_GATEWAY_CREDENTIAL_NAME,
DISCOVERY_TOOL_CALL_MULTI_TAG,
DISCOVERY_TOOL_CALL_SINGLE_TAG,
DiscoveredModel,
PROVIDER_MODEL_SOURCES,
ProviderModelSource,
agent_from_discovered,
agent_id_for,
configured_gateway_source,
discover_all_models,
discovery_tool_call_tags,
free_discovered_models,
general_free_serving_candidates,
is_discovered_chat_candidate,
Expand All @@ -40,6 +43,7 @@
from .orchestrator import (
CONTEXTUAL_ORCHESTRATOR_CONTRACT_V1,
MAX_LOCAL_CONCURRENCY,
ModelAgent,
ModelClient,
TaskOrchestrator,
load_agents,
Expand Down Expand Up @@ -689,6 +693,56 @@ def _discover_models_command(argv: list[str]) -> None:
raise SystemExit(1)


_DISCOVERY_CAPABILITY_BLOCKED_TAG = "discovery:blocked:capability"
_DISCOVERY_CAPABILITY_PRESERVE_DISABLED_TAG = (
"discovery:blocked:capability:preserve-disabled"
)


def _refresh_discovered_tool_call_tags(
tags: tuple[str, ...],
model: DiscoveredModel,
) -> tuple[str, ...]:
"""Replace stale tool-call evidence with the current discovery result.

Only the specific visible tag paired with a discovery marker currently
present is discovery-owned. A prior discovery pass always writes its
visible tag and hidden marker together (see
:func:`~contextual_orchestrator.model_discovery.discovery_tool_call_tags`),
so a marker for one polarity (e.g. ``discovery:tool_call:multi``) never
implies ownership of the *other* visible tag. This preserves an
operator-authored ``tool_call:single``/``tool_call:multi`` override that
predates any discovery evidence: it is never removed just because
discovery later supplies -- and then withdraws -- unrelated evidence.

A same-polarity collision (an operator's plain tag already reads the
same value discovery now independently reports) is handled the same
way: discovery never claims ownership -- by adding its hidden marker --
of a visible tag that is already present without one. The two tags are
string-identical, so there would otherwise be no way to tell them apart
on a later refresh; leaving the marker off means the pre-existing tag
is never mistaken for discovery's own and never gets removed just
because discovery's evidence later goes stale.
"""
discovery_owned_visible_tag = {
DISCOVERY_TOOL_CALL_SINGLE_TAG: "tool_call:single",
DISCOVERY_TOOL_CALL_MULTI_TAG: "tool_call:multi",
}
hidden_tags = set(discovery_owned_visible_tag)
owned_visible_tags = {
discovery_owned_visible_tag[tag] for tag in tags if tag in hidden_tags
}
Comment thread
devin-ai-integration[bot] marked this conversation as resolved.
refreshed = [
tag
for tag in tags
if tag not in hidden_tags and tag not in owned_visible_tags
]
new_evidence_tags = discovery_tool_call_tags(model)
if new_evidence_tags and new_evidence_tags[0] in refreshed:
return tuple(dict.fromkeys(refreshed))
return tuple(dict.fromkeys(refreshed)) + new_evidence_tags


def _probe_configured_gateway_structured_chat(
orchestrator: TaskOrchestrator,
model: DiscoveredModel,
Expand Down Expand Up @@ -730,7 +784,10 @@ def _auto_discover_runtime_agents(orchestrator: TaskOrchestrator) -> dict[str, l
chat_models = [
model
for model in discovered
if not model.evidence_only and is_discovered_chat_candidate(model)
if not model.evidence_only
and is_discovered_chat_candidate(
replace(model, supports_parallel_tool_calls=None)
)
Comment thread
seonghobae marked this conversation as resolved.
]
configured_gateway_probe_required = any(
model.provider_name == "configured_gateway"
Expand Down Expand Up @@ -767,12 +824,28 @@ def _auto_discover_runtime_agents(orchestrator: TaskOrchestrator) -> dict[str, l
agents = []
for model in runtime_models:
existing = existing_by_id.get(agent_id_for(model))
capability_blocked = model.supports_parallel_tool_calls is False
embedding_routable = "embedding" in model.capabilities and model.spend_admitted
spend_routable = is_routable_discovered_model(model) or embedding_routable
spend_routable = (
is_routable_discovered_model(replace(model, supports_parallel_tool_calls=None))
or embedding_routable
)
structured_routable = agent_id_for(model) not in failed_configured_gateway_probe_ids
routable = embedding_routable or (spend_routable and structured_routable)
routable = embedding_routable or (
spend_routable and structured_routable and not capability_blocked
)
if existing is None:
agents.append(replace(agent_from_discovered(model), disabled=not routable))
agent = replace(
agent_from_discovered(
replace(model, supports_parallel_tool_calls=None)
),
disabled=not routable,
)
tags = _refresh_discovered_tool_call_tags(agent.tags, model)
if capability_blocked:
tags = (*tags, _DISCOVERY_CAPABILITY_BLOCKED_TAG)
agent = replace(agent, tags=tuple(dict.fromkeys(tags)))
agents.append(agent)
elif "discovered" not in existing.tags:
continue
else:
Expand All @@ -786,21 +859,26 @@ def _auto_discover_runtime_agents(orchestrator: TaskOrchestrator) -> dict[str, l
if model.context_window_conflicted
else model.context_window or existing.context_window
)
refreshed_tags = _refresh_discovered_tool_call_tags(existing.tags, model)
limits_changed = (
existing.max_output_tokens != max_output_tokens
or existing.context_window != context_window
or refreshed_tags != existing.tags
)
existing = replace(
existing,
max_output_tokens=max_output_tokens,
context_window=context_window,
tags=refreshed_tags,
)
if existing is not None and (not routable or not structured_routable):
block_markers = {
"spend:blocked",
"spend:blocked:preserve-disabled",
"structured:blocked",
"structured:blocked:preserve-disabled",
_DISCOVERY_CAPABILITY_BLOCKED_TAG,
_DISCOVERY_CAPABILITY_PRESERVE_DISABLED_TAG,
}
preserve_disabled = existing.disabled and (
not block_markers.intersection(existing.tags)
Expand All @@ -811,6 +889,8 @@ def _auto_discover_runtime_agents(orchestrator: TaskOrchestrator) -> dict[str, l
blocked_tags.append("spend:blocked")
if not structured_routable:
blocked_tags.append("structured:blocked")
if capability_blocked:
blocked_tags.append(_DISCOVERY_CAPABILITY_BLOCKED_TAG)
tags = (
*(tag for tag in existing.tags if tag not in block_markers),
*blocked_tags,
Expand All @@ -824,7 +904,14 @@ def _auto_discover_runtime_agents(orchestrator: TaskOrchestrator) -> dict[str, l
tags=tuple(dict.fromkeys(tags)),
)
)
elif existing is not None and any(tag in existing.tags for tag in ("spend:blocked", "structured:blocked")):
elif existing is not None and any(
tag in existing.tags
for tag in (
"spend:blocked",
"structured:blocked",
_DISCOVERY_CAPABILITY_BLOCKED_TAG,
)
):
agents.append(
replace(
existing,
Expand All @@ -833,6 +920,7 @@ def _auto_discover_runtime_agents(orchestrator: TaskOrchestrator) -> dict[str, l
for tag in (
"spend:blocked:preserve-disabled",
"structured:blocked:preserve-disabled",
_DISCOVERY_CAPABILITY_PRESERVE_DISABLED_TAG,
)
),
tags=tuple(
Expand All @@ -844,6 +932,8 @@ def _auto_discover_runtime_agents(orchestrator: TaskOrchestrator) -> dict[str, l
"spend:blocked:preserve-disabled",
"structured:blocked",
"structured:blocked:preserve-disabled",
_DISCOVERY_CAPABILITY_BLOCKED_TAG,
_DISCOVERY_CAPABILITY_PRESERVE_DISABLED_TAG,
}
),
)
Expand Down
8 changes: 8 additions & 0 deletions contextual_orchestrator/chat_capability.py
Original file line number Diff line number Diff line change
Expand Up @@ -127,13 +127,21 @@ def is_general_chat_candidate(
*,
capabilities: Iterable[str] = (),
output_modalities: Iterable[str] = (),
supports_parallel_tool_calls: bool | None = None,
) -> bool:
"""Apply explicit catalog evidence before falling back to the model name.

A generic model identifier cannot identify a media-only endpoint. When a
provider supplies capability or output-modality metadata, that metadata is
therefore authoritative; absent metadata keeps the legacy name heuristic.

General chat agents may receive multi-tool-call requests, so a model whose
catalog or probe evidence shows it only supports one tool call at a time is
not a general chat candidate. ``None`` means no evidence either way and
keeps the existing eligibility decision.
"""
if supports_parallel_tool_calls is False:
return False
Comment thread
seonghobae marked this conversation as resolved.
outputs = {
value.strip().casefold()
for value in output_modalities
Expand Down
Loading
Loading