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
6 changes: 4 additions & 2 deletions backend/druks/api/dashboard.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
from druks.apps.loader import iter_apps
from druks.database import db_session
from druks.durable.enums import OPEN_STATES, RunState
from druks.durable.models import Run
from druks.durable.models import Artifact, Run
from druks.user_settings.models import UserSettings

PAGE_SIZE = 200
Expand Down Expand Up @@ -36,10 +36,12 @@ async def list_current_work(response: Response) -> DashboardWork:
current.c.updated_at,
current.c.input_requested_at.label("parked_at"),
current.c.request_label,
func.left(Artifact.title, 240).label("artifact_title"),
current.c.presentation,
current.c.request_url,
func.left(current.c.failure, 512).label("failure"),
func.left(current.c.failure, 2048).label("failure"),
)
.outerjoin(Artifact, Artifact.agent_call_id == current.c.latest_call_id)
.order_by(current.c.updated_at.desc(), current.c.run_id.desc())
.limit(PAGE_SIZE + 1)
)
Expand Down
1 change: 1 addition & 0 deletions backend/druks/api/schemas.py
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ class DashboardRun(Schema):
updated_at: datetime
parked_at: datetime | None
request_label: str | None
artifact_title: str | None
presentation: str | None
request_url: str | None
failure: str | None
Expand Down
1 change: 1 addition & 0 deletions backend/druks/apps/routes.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ async def list_apps() -> list[AppResponse]:
path=f"/{app.name}{page.route}".rstrip("/"),
parent=page.parent.name if page.parent else "",
order=declaration_order[page.name],
subject_type=page.subject.subject_type if page.subject else "",
)
for page in pages
],
Expand Down
1 change: 1 addition & 0 deletions backend/druks/apps/schemas.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ class PageEntry(Schema):
path: str
parent: str
order: int
subject_type: str = ""


class Operation(Schema):
Expand Down
17 changes: 17 additions & 0 deletions backend/druks/apps/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,22 @@ def field_multiline(field: FieldInfo) -> bool:
return False


def validate_field_choice_details(field: FieldInfo) -> dict[str, dict[str, str]]:
# A key outside the Literal shows as a raw value in the form. Druks rejects it here.
metadata = field.json_schema_extra
details: dict[str, dict[str, str]] = {}
if isinstance(metadata, dict):
details = metadata.get("choice_details", {})
choices = field_choices(field) or []
unknown = sorted(set(details) - set(choices))
if unknown:
raise SettingsDeclarationError(
f"choice_details keys {unknown!r} are not declared choices. "
f"Use values from {choices!r}."
)
return details


def field_visibility(field: FieldInfo) -> tuple[str, Any]:
# The sibling field this one is shown for and the value that field must hold. The
# name is empty when the field is always shown.
Expand Down Expand Up @@ -101,6 +117,7 @@ def validate_settings_declaration(model: type[BaseModel]) -> None:
# a shape the plane can't render (or safely redact) fails loudly where it's written
# rather than at the first operator PATCH.
for name, field in model.model_fields.items():
validate_field_choice_details(field)
if nested := _nested_model(field.annotation):
raise SettingsDeclarationError(
f"settings field {name!r}: nested models are not a supported settings "
Expand Down
35 changes: 27 additions & 8 deletions backend/druks/contrib/software_factory/workflows.py
Original file line number Diff line number Diff line change
Expand Up @@ -75,14 +75,33 @@ class Settings(BaseModel):
plan_gate: PlanGate = Field(
default="human",
title="Plan gate",
description=(
"human — Operator reviews every plan; the machine reviewer never runs. "
"machine — The machine reviewer critiques once; the plan implements without "
"operator review. machine_then_human — The machine reviewer critiques once, "
"then the operator approves every plan. adaptive — The machine reviewer "
"critiques once; a high-confidence plan it approved implements directly, "
"anything less parks for the operator."
),
description="Choose who approves the plan before implementation.",
json_schema_extra={
"choice_details": {
"human": {
"label": "Human review",
"help": "You approve every plan. The machine reviewer does not run.",
},
"machine": {
"label": "Machine review",
"help": (
"The machine reviewer checks once. "
"Implementation starts without your approval."
),
},
"machine_then_human": {
"label": "Machine then human",
"help": "The machine reviewer checks once. You then approve the plan.",
},
"adaptive": {
"label": "Adaptive review",
"help": (
"An approved high-confidence plan starts directly. "
"All other plans need your approval."
),
},
},
},
)
max_implementation_revisions: int = Field(
default=5,
Expand Down
36 changes: 32 additions & 4 deletions backend/druks/ui/page.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,17 @@
from collections.abc import Awaitable, Callable
from inspect import Parameter, signature
from itertools import count
from typing import TYPE_CHECKING

from druks.apps.registry import pages

from .exceptions import PageRouteError
from .schemas import Page

if TYPE_CHECKING:
from druks.durable.datastructures import Subject
from druks.models import StoredSubject

PageFunction = Callable[..., Awaitable[Page]]

# A route parameter — ``{note_id}``, or the catch-all ``{rest:path}``.
Expand Down Expand Up @@ -35,6 +40,7 @@ def __init__(
*,
label: str = "",
parent: "PageRoute | None" = None,
subject: "type[Subject] | type[StoredSubject] | None" = None,
) -> None:
self.path = path
self.function = function
Expand All @@ -43,6 +49,7 @@ def __init__(
self.module = function.__module__
self.label = label or self.name.replace("_", " ")
self.order = next(_declared)
self.subject = subject

def child(self, path: str, *, label: str = "") -> Callable[[PageFunction], "PageRoute"]:
"""Declare a page under this one, at ``path`` relative to this route."""
Expand Down Expand Up @@ -95,7 +102,13 @@ def check(self, app_name: str) -> None:
"is not the last segment, so it would swallow every route under it. Put the "
"catch-all last."
)
route_parameters = {name for name, _ in _PARAMETER.findall(self.route)}
parameters = _PARAMETER.findall(self.route)
route_parameters = {name for name, _ in parameters}
if self.subject and len(parameters) != 1:
raise PageRouteError(
f"page {self.name!r} declares a subject destination. "
"Use exactly one route parameter for its subject id."
)
declared = signature(self.function).parameters
by_name = {
name for name, parameter in declared.items() if parameter.kind in _CALLABLE_BY_NAME
Expand Down Expand Up @@ -123,12 +136,18 @@ def match_key(self) -> tuple[tuple[int, str], ...]:
return tuple(key)


def page(path: str, *, label: str = "") -> Callable[[PageFunction], PageRoute]:
def page(
path: str,
*,
label: str = "",
subject: "type[Subject] | type[StoredSubject] | None" = None,
) -> Callable[[PageFunction], PageRoute]:
"""Declare a top-level page at ``path``. The label defaults to the function
name with its underscores as spaces."""
name with its underscores as spaces. ``subject`` selects this page for that
subject type's Dashboard decisions. Its route must have one ID parameter."""

def declare(function: PageFunction) -> PageRoute:
return pages.register(PageRoute(path, function, label=label))
return pages.register(PageRoute(path, function, label=label, subject=subject))

return declare

Expand All @@ -154,8 +173,17 @@ def list_pages_for_app(app_name: str, package: str) -> list[PageRoute]:

by_name: dict[str, PageRoute] = {}
by_shape: dict[str, PageRoute] = {}
by_subject: dict[str, PageRoute] = {}
for page_route in declared:
page_route.check(app_name)
if page_route.subject:
subject_type = page_route.subject.subject_type
if subject_type in by_subject:
raise PageRouteError(
f"app {app_name!r} declares two destinations for {subject_type!r}. "
"Declare one subject decision page."
)
by_subject[subject_type] = page_route
clash = by_name.get(page_route.name)
if clash:
raise PageRouteError(
Expand Down
3 changes: 3 additions & 0 deletions backend/druks/user_settings/schemas.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
field_multiline,
field_section,
field_visibility,
validate_field_choice_details,
)
from druks.harnesses.datastructures import Billing
from druks.harnesses.schemas import SortedNames
Expand Down Expand Up @@ -95,6 +96,7 @@ class SettingsFieldResponse(Schema):
default: Any
# An enum field's allowed values; None for every other kind.
choices: list[str] | None
choice_details: dict[str, dict[str, str]] = {}
# The heading this field groups under; empty for an ungrouped one.
section: str
# The sibling field this one is shown for, and the value that field must hold. The
Expand Down Expand Up @@ -125,6 +127,7 @@ def from_field(
value=None if secret else value,
default=None if secret else field.default,
choices=field_choices(field),
choice_details=validate_field_choice_details(field),
section=field_section(field),
visible_when_field=controller,
visible_when_value=target,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -157,7 +157,7 @@ async def new_note():
)


@ui.page("/notes/{note_id}")
@ui.page("/notes/{note_id}", subject=Note)
async def note(note_id: int):
found = await Note.get(note_id)
if found:
Expand Down
50 changes: 26 additions & 24 deletions backend/tests/test_api_settings.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
from pathlib import Path

from druks.accounts.models import Account
from druks.contrib.software_factory.app import SoftwareFactory
from druks.database import db_session
from druks.testing import configure_app_for_test, make_settings
Expand Down Expand Up @@ -72,8 +73,6 @@ def test_patch_settings_judges_the_default_triple_together(tmp_path: Path):


async def test_patch_settings_sets_the_account_unattended_runs_run_as(tmp_path: Path, druks_db):
from druks.accounts.models import Account

account = await Account.get_or_create("ops@example.com")
with _build_client(tmp_path) as client:
assert {"id": account.id, "username": "ops@example.com"} in client.get(
Expand All @@ -90,7 +89,7 @@ async def test_patch_settings_sets_the_account_unattended_runs_run_as(tmp_path:

def test_patch_settings_persists_valid_iana_zone(tmp_path: Path, monkeypatch):
async def _noop_schedules():
return None
return

monkeypatch.setattr("druks.user_settings.routes.apply_schedules", _noop_schedules)
with _build_client(tmp_path) as client:
Expand Down Expand Up @@ -210,21 +209,17 @@ def test_agents_lists_every_apps_agents_as_they_resolve(tmp_path: Path):

def test_apps_judge_an_agents_triple_as_it_resolves(tmp_path: Path):
with _build_client(tmp_path) as client:
# A key-only harness with the inherited subscription billing.
response = client.patch("/api/settings/apps", json={"agentHarnesses": {"implement": "pi"}})
assert response.status_code == 422
assert "API key only" in response.json()["detail"]
# A model outside the inherited harness's vendor.
response = client.patch(
"/api/settings/apps", json={"agentModels": {"implement": "openai/gpt-5.5"}}
)
assert response.status_code == 422
assert "does not run OpenAI" in response.json()["detail"]
# The rejected writes never landed.
agents = {a["name"]: a for a in _software_factory_app(client)["agents"]}
assert agents["implement"]["harnessSource"] == "default"
assert agents["implement"]["source"] == "default"
# Both cells together fit.
response = client.patch(
"/api/settings/apps",
json={"agentHarnesses": {"implement": "pi"}, "agentBillings": {"implement": "api_key"}},
Expand All @@ -233,7 +228,6 @@ def test_apps_judge_an_agents_triple_as_it_resolves(tmp_path: Path):
agents = {a["name"]: a for a in _software_factory_app(client)["agents"]}
assert (agents["implement"]["harness"], agents["implement"]["billing"]) == ("pi", "api_key")
assert agents["implement"]["billingSource"] == "agent"
# An agent nobody registered.
response = client.patch("/api/settings/apps", json={"agentBillings": {"ghost": "api_key"}})
assert response.status_code == 422

Expand All @@ -254,7 +248,6 @@ def test_apps_surface_build_agents(tmp_path: Path):
apps = {m["name"]: m for m in body["apps"]}

build_agents = {a["name"]: a for a in apps["software_factory"]["agents"]}
# The build pipeline's plan stage stays; the standalone Plan-tab agent is gone.
assert "generate_plan" in build_agents
assert "planning" not in build_agents

Expand All @@ -264,7 +257,6 @@ def test_apps_surface_build_agents_and_workflow_defaults(tmp_path: Path):
build = _software_factory_app(client)

agents = {a["name"]: a for a in build["agents"]}
# Every cell inherits the operator's defaults when no override is set.
assert agents["generate_plan"] == {
"name": "generate_plan",
"description": "ticket → implementation plan",
Expand All @@ -281,20 +273,35 @@ def test_apps_surface_build_agents_and_workflow_defaults(tmp_path: Path):
}
assert agents["implement"]["model"] == "anthropic/claude-opus-4-7"
assert agents["evaluate_implementation"]["effortSource"] == "default"
# The workflow's settings surface alongside its agents.
fields = {f["name"]: f for f in build["workflows"][0]["fields"]}
assert fields["max_implementation_revisions"]["value"] == 5
assert fields["plan_gate"] == {
"name": "plan_gate",
"label": "Plan gate",
"help": (
"human — Operator reviews every plan; the machine reviewer never runs. "
"machine — The machine reviewer critiques once; the plan implements without "
"operator review. machine_then_human — The machine reviewer critiques once, "
"then the operator approves every plan. adaptive — The machine reviewer "
"critiques once; a high-confidence plan it approved implements directly, "
"anything less parks for the operator."
),
"help": "Choose who approves the plan before implementation.",
"choiceDetails": {
"human": {
"label": "Human review",
"help": "You approve every plan. The machine reviewer does not run.",
},
"machine": {
"label": "Machine review",
"help": (
"The machine reviewer checks once. Implementation starts without your approval."
),
},
"machine_then_human": {
"label": "Machine then human",
"help": "The machine reviewer checks once. You then approve the plan.",
},
"adaptive": {
"label": "Adaptive review",
"help": (
"An approved high-confidence plan starts directly. "
"All other plans need your approval."
),
},
},
"type": "enum",
"value": "human",
"default": "human",
Expand Down Expand Up @@ -518,11 +525,9 @@ def test_apps_default_effort_and_per_agent_effort_override(tmp_path: Path):
assert agents["generate_plan"]["effort"] == "high"
assert agents["generate_plan"]["effortSource"] == "default"

# Retune the default effort + override one agent.
client.patch("/api/settings", json={"defaultEffort": "low"})
client.patch("/api/settings/apps", json={"agentEfforts": {"generate_plan": "high"}})
agents = {a["name"]: a for a in _software_factory_app(client)["agents"]}
# generate_plan overridden; revise_contract inherits "low".
assert agents["generate_plan"]["effort"] == "high"
assert agents["generate_plan"]["effortSource"] == "agent"
assert agents["revise_contract"]["effort"] == "low"
Expand All @@ -545,11 +550,9 @@ def test_apps_default_timeout_and_per_agent_timeout_override(tmp_path: Path):
assert agents["implement"]["timeout"] == 1800
assert agents["implement"]["timeoutSource"] == "default"

# Retune the default timeout + override one agent.
client.patch("/api/settings", json={"defaultTimeout": 1200})
client.patch("/api/settings/apps", json={"agentTimeouts": {"implement": 3600}})
agents = {a["name"]: a for a in _software_factory_app(client)["agents"]}
# implement overridden; review_plan inherits 1200.
assert agents["implement"]["timeout"] == 3600
assert agents["implement"]["timeoutSource"] == "agent"
assert agents["review_plan"]["timeout"] == 1200
Expand Down Expand Up @@ -593,7 +596,6 @@ def test_apps_clearing_an_override_reverts_to_the_operator_default(tmp_path: Pat
assert agents["generate_plan"]["model"] == "anthropic/claude-opus-4-7"
assert agents["generate_plan"]["source"] == "agent"

# Null clears the override; the agent falls back to the operator default.
client.patch("/api/settings/apps", json={"agentModels": {"generate_plan": None}})
agents = {a["name"]: a for a in _software_factory_app(client)["agents"]}
assert agents["generate_plan"]["model"] == "anthropic/claude-opus-4-7"
Expand Down
Loading