Inform AI engine which endpoints are disabled on the backend (#6251)

# Description of Changes
Have the Java send a list of enabled endpoints to the AI engine so it
can intelligently respond to the user that the tool does exist but is
disabled on the server so it can't acutally run the operation, instead
of the current behaviour where it sends the API call back and then 503
errors because the execution fails when the URL is disabled.

<img width="380" height="208" alt="image"
src="https://github.com/user-attachments/assets/5842fb2e-2e55-45a5-8205-25515636daae"
/>

---------

Co-authored-by: EthanHealy01 <[email protected]>
This commit is contained in:
James Brunton
2026-05-01 14:59:53 +00:00
committed by GitHub
co-authored by EthanHealy01
parent 5541dd666c
commit 51f5345151
13 changed files with 418 additions and 95 deletions
+12
View File
@@ -1,5 +1,6 @@
from __future__ import annotations
from collections.abc import Iterable
from enum import StrEnum
from typing import Literal, assert_never
@@ -197,3 +198,14 @@ class ToolOperationStep(ApiModel):
actual_type = type(self.parameters).__name__
raise ValueError(f"Parameters for tool {self.tool} must be {expected_type.__name__}, got {actual_type}.")
return self
def drop_unknown_tool_endpoints(value: Iterable[str | ToolEndpoint]) -> list[ToolEndpoint]:
"""Coerce inbound endpoint identifiers into `ToolEndpoint` members, dropping unknowns.
Java sends the full set of endpoints it considers enabled. The engine and the Java
backend may have version drift in either direction, so we silently drop anything we
don't recognise rather than failing the request. Anything dropped simply doesn't
appear as a supported tool to the planner.
"""
return [ToolEndpoint(item) for item in value if item in ToolEndpoint]
@@ -2,9 +2,9 @@ from __future__ import annotations
from typing import Annotated, Literal
from pydantic import Field
from pydantic import BeforeValidator, Field
from stirling.models import ApiModel
from stirling.models import ApiModel, ToolEndpoint
from .agent_drafts import AgentDraftResponse
from .common import (
@@ -17,6 +17,7 @@ from .common import (
SupportedCapability,
ToolReportArtifact,
WorkflowOutcome,
drop_unknown_tool_endpoints,
)
from .execution import NextExecutionAction
from .pdf_edit import PdfEditTerminalResponse
@@ -37,6 +38,10 @@ class OrchestratorRequest(ApiModel):
conversation_history: list[ConversationMessage] = Field(default_factory=list)
artifacts: list[WorkflowArtifact] = Field(default_factory=list)
resume_with: SupportedCapability | None = None
# See `PdfEditRequest.enabled_endpoints`.
enabled_endpoints: Annotated[list[ToolEndpoint], BeforeValidator(drop_unknown_tool_endpoints)] = Field(
default_factory=list
)
class UnsupportedCapabilityResponse(ApiModel):
+10 -2
View File
@@ -2,9 +2,9 @@ from __future__ import annotations
from typing import Annotated, Literal
from pydantic import Field
from pydantic import BeforeValidator, Field
from stirling.models import ApiModel
from stirling.models import ApiModel, ToolEndpoint
from .common import (
AiFile,
@@ -14,6 +14,7 @@ from .common import (
SupportedCapability,
ToolOperationStep,
WorkflowOutcome,
drop_unknown_tool_endpoints,
)
@@ -22,6 +23,13 @@ class PdfEditRequest(ApiModel):
files: list[AiFile] = Field(default_factory=list)
conversation_history: list[ConversationMessage] = Field(default_factory=list)
page_text: list[ExtractedFileText] = Field(default_factory=list)
# The set of endpoints the Java backend considers usable. Unknown URLs are silently
# dropped so the engine and Java can drift in either direction without breaking
# validation. An empty list means no operations are available - the planner will
# return `cannot_do`.
enabled_endpoints: Annotated[list[ToolEndpoint], BeforeValidator(drop_unknown_tool_endpoints)] = Field(
default_factory=list
)
class EditPlanResponse(ApiModel):