Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
7 changes: 0 additions & 7 deletions backend/app/api/enterprise.py
Original file line number Diff line number Diff line change
Expand Up @@ -936,7 +936,6 @@ async def _runtime_model_settings_payload(db: AsyncSession, *, tenant_id: uuid.U
or_(LLMModel.tenant_id.is_(None), LLMModel.tenant_id == tenant_id),
LLMModel.enabled.is_(True),
LLMModel.deleted_at.is_(None),
LLMModel.supports_tool_calling.is_(True),
)
.order_by(LLMModel.created_at.desc())
)
Expand Down Expand Up @@ -1000,12 +999,6 @@ async def update_runtime_model_settings(
raise HTTPException(status_code=422, detail=f"Model {model_id} belongs to another tenant")
if not model.enabled:
raise HTTPException(status_code=422, detail=f"Model {model_id} is disabled")
if model.supports_tool_calling is not True:
raise HTTPException(
status_code=422,
detail=f"Model {model_id} has not passed the native tool-calling test",
)

result = await db.execute(
select(SystemSetting).where(
SystemSetting.key == runtime_model_setting_key(resolved_tenant_id)
Expand Down
8 changes: 8 additions & 0 deletions backend/app/api/groups.py
Original file line number Diff line number Diff line change
Expand Up @@ -1596,6 +1596,14 @@ async def download_group_workspace_file(
}
if allow_inline and media_type == "image/svg+xml":
headers["Content-Security-Policy"] = "sandbox; default-src 'none'; style-src 'unsafe-inline'"
_stage_audit(
db,
current_user=current_user,
action="group:workspace_download",
tenant_id=tenant_id,
group_id=group_id,
details={"path": value.path, "inline": allow_inline},
)
return Response(content=value.content, media_type=media_type, headers=headers)


Expand Down
10 changes: 9 additions & 1 deletion backend/app/api/websocket.py
Original file line number Diff line number Diff line change
Expand Up @@ -162,6 +162,14 @@ async def is_user_viewing_session(self, agent_id: str, session_id: str, user_id:
manager = ConnectionManager()


def _websocket_content_log_summary(content: object) -> str:
"""Return payload-free metadata for one inbound WebSocket message."""
if not isinstance(content, str):
return f"content_type={type(content).__name__}"
image_count = content.count("[image_data:data:image/")
return f"content_chars={len(content)} image_count={image_count}"


def _runtime_error_packet(
*,
code: str,
Expand Down Expand Up @@ -680,7 +688,7 @@ async def _accept_client_message(
override_model_id = data.get("model_id")
is_onboarding_trigger = data.get("kind") == "onboarding_trigger"
logger.info(
f"[WS] Received: {str(content)[:50]}"
f"[WS] Received: {_websocket_content_log_summary(content)}"
+ (" [onboarding]" if is_onboarding_trigger else "")
)
if not isinstance(content, str) or (not content and not is_onboarding_trigger):
Expand Down
18 changes: 3 additions & 15 deletions backend/app/services/agent_runtime/adapter.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,10 +20,6 @@
StartRunCommand,
)
from app.services.agent_runtime.graph import RuntimeGraphIdentity
from app.services.agent_runtime.model_capabilities import (
ModelCapabilityError,
ModelCapabilityResolver,
)
from app.services.agent_runtime.persistence import (
RunRegistration,
enqueue_cancel,
Expand Down Expand Up @@ -156,7 +152,7 @@ async def _require_agent_runtime_model(
command: StartRunCommand,
agent: Agent | None,
) -> uuid.UUID | None:
"""Reject new tool-driven Agent Runs before any durable Run is created."""
"""Pin an active tenant-valid model before a durable Run is created."""
if command.run_kind == "orchestration":
return command.model_id
model = await load_active_model(
Expand All @@ -165,20 +161,12 @@ async def _require_agent_runtime_model(
tenant_id=command.tenant_id,
)
if model is None and agent is not None:
model = await resolve_active_agent_model(
self._db,
agent,
require_tool_calling=True,
)
model = await resolve_active_agent_model(self._db, agent)
if model is None:
raise RuntimeAdapterError(
"model_unavailable",
"Agent Runtime has no active tool-capable model in the command tenant",
"Agent Runtime has no active model in the command tenant",
)
try:
ModelCapabilityResolver.require_native_tool_calling(model)
except ModelCapabilityError as exc:
raise RuntimeAdapterError(exc.code, str(exc)) from exc
return model.id

@staticmethod
Expand Down
15 changes: 15 additions & 0 deletions backend/app/services/agent_runtime/checkpoint_side_effects.py
Original file line number Diff line number Diff line change
Expand Up @@ -467,6 +467,21 @@ async def _record_lifecycle_events(
agent_id = uuid.UUID(run.agent_id) if run.agent_id is not None else None
events: list[tuple[str, str, dict, str, str | None]] = []
if checkpoint is None:
terminal_result = await db.execute(
select(AgentRunEvent.id)
.where(
AgentRunEvent.tenant_id == run.tenant_id,
AgentRunEvent.run_id == run.run_id,
AgentRunEvent.event_type.in_(
("run_completed", "run_failed", "run_cancelled")
),
)
.limit(1)
)
if terminal_result.scalar_one_or_none() is not None:
# A later cancel command may release control resources, but it must
# not append a second, contradictory terminal outcome.
return
events.append(
(
"run_cancelled",
Expand Down
23 changes: 4 additions & 19 deletions backend/app/services/agent_runtime/delivery.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,9 @@
from app.services.participant_identity import get_or_create_agent_participant


# ``ack`` remains readable only for historical delivery receipts created before
# native Group start acknowledgements were retired. New requests accept only
# waiting and terminal delivery kinds.
DeliveryKind = Literal["ack", "waiting", "terminal"]
DeliveryLifecycleStatus = Literal[
"waiting_user",
Expand Down Expand Up @@ -85,8 +88,6 @@ class DeliveryRequest:

@property
def idempotency_key(self) -> str:
if self.kind == "ack":
return f"run:{self.run_id}:ack"
if self.kind == "waiting":
return f"run:{self.run_id}:waiting:{self.interrupt_id}"
return f"run:{self.run_id}:terminal:{self.lifecycle_status}"
Expand Down Expand Up @@ -152,7 +153,7 @@ def _require_text(


def _validate_request(request: DeliveryRequest) -> None:
if request.kind not in {"ack", "waiting", "terminal"}:
if request.kind not in {"waiting", "terminal"}:
raise DeliveryServiceError(
"invalid_delivery_request",
f"unsupported delivery kind: {request.kind!r}",
Expand All @@ -162,22 +163,6 @@ def _validate_request(request: DeliveryRequest) -> None:
"invalid_delivery_request",
"original_target_outcome must be not_attempted or unknown",
)
if request.kind == "ack":
_require_text(request.content, field="content")
if (
request.checkpoint_id is not None
or request.lifecycle_status is not None
or request.interrupt_id is not None
or request.group_handoff_intent is not None
or request.failure_code is not None
or request.failure_message is not None
):
raise DeliveryServiceError(
"invalid_delivery_request",
"ack delivery cannot carry checkpoint lifecycle fields",
)
return

_require_text(request.checkpoint_id, field="checkpoint_id", max_length=255)
if request.kind == "waiting":
_require_text(request.content, field="content")
Expand Down
84 changes: 0 additions & 84 deletions backend/app/services/agent_runtime/group_acknowledgement.py

This file was deleted.

15 changes: 13 additions & 2 deletions backend/app/services/agent_runtime/group_handoff.py
Original file line number Diff line number Diff line change
Expand Up @@ -395,8 +395,8 @@ def _source_run_matches(
or source_run.system_role is not None
or source_run.runtime_type != "langgraph"
or source_run.runtime_thread_id != str(source_run.id)
# Group start acknowledgement may already be delivered while the Run
# remains active. This projection is not a Runtime lifecycle state.
# Historical Runs may already have delivered the retired start ACK;
# current Runs remain pending until waiting or terminal delivery.
or source_run.delivery_status not in {"pending", "delivered"}
):
raise GroupAgentHandoffError(
Expand Down Expand Up @@ -517,6 +517,17 @@ async def _validate_targets(
"Group mention resolution did not preserve the frozen participant order",
repairable=True,
)
self_targets = [
mention.participant_id
for mention in resolved
if mention.agent is not None and mention.agent.id == source_agent_id
]
if self_targets:
raise GroupAgentHandoffError(
"group_handoff_self_target",
"An Agent cannot create a public handoff to itself",
repairable=True,
)
for mention in resolved:
assert mention.agent is not None
if not _target_budget_available(mention.agent, now=clock):
Expand Down
22 changes: 0 additions & 22 deletions backend/app/services/agent_runtime/model_capabilities.py
Original file line number Diff line number Diff line change
Expand Up @@ -76,28 +76,6 @@ def _legacy_output_limit(value: int | None) -> int | None:
class ModelCapabilityResolver:
"""Resolve model semantics without performing provider I/O."""

@staticmethod
def require_native_tool_calling(model: LLMModel) -> None:
"""Require a concrete model row to be safe for an Agent tool Runtime."""
model_label = getattr(model, "label", None) or model.model
if model.supports_tool_calling is True:
return
if model.supports_tool_calling is False:
raise ModelCapabilityError(
"model_tool_calling_unsupported",
(
f"Model {model_label!r} did not produce a valid "
"native tool call during its capability test."
),
)
raise ModelCapabilityError(
"model_tool_calling_unverified",
(
f"Model {model_label!r} has not passed the native "
"tool-calling capability test required by Agent Runtime."
),
)

@staticmethod
def capabilities(
model: LLMModel,
Expand Down
Loading