Skip to content
Open
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
17 changes: 17 additions & 0 deletions packages/backend/app/test/test_agent_orchestration.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
_normalize_workflow_run_result,
_require_reuse_after_failure,
create_workflow_and_run_tool,
create_read_workflow_tool,
create_update_workflow_tool,
create_design_workflow_tool,
create_summarize_workflow_result_tool,
Expand Down Expand Up @@ -726,6 +727,22 @@ def test_workflow_tools_expose_planning_notes_field() -> None:
assert "planning_notes" in update_schema["properties"]


def test_workflow_tool_optional_strings_use_plain_string_schema() -> None:
tools = [
create_workflow_and_run_tool("session-1"),
create_update_workflow_tool("session-1", "user-1"),
create_read_workflow_tool("session-1"),
]

for tool_obj in tools:
schema = tool_obj.args_schema.model_json_schema()
for field_name in ("draft_id", "name", "file_path", "planning_notes"):
field_schema = schema["properties"].get(field_name)
if field_schema is not None:
assert field_schema.get("type") == "string"
assert "anyOf" not in field_schema


@pytest.mark.anyio
async def test_workflow_agent_uses_compact_toolset(monkeypatch) -> None:
captured: dict[str, object] = {}
Expand Down
63 changes: 37 additions & 26 deletions packages/backend/app/tools/workflow_tools.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,13 @@
from deepeye.tools.base import tool


def _empty_to_none(value: str | None) -> str | None:
if value is None:
return None
value = value.strip()
return value or None


def _build_run_failure_response(
outcome: WorkflowAgentRunOutcome,
repair_state: dict[str, Any] | None,
Expand Down Expand Up @@ -79,10 +86,10 @@ def create_create_workflow_tool(session_id: str, user_id: str, turn_id: str | No
@tool
async def create_workflow(
workflow: dict,
draft_id: str | None = None,
name: str | None = None,
file_path: str | None = None,
planning_notes: str | None = None,
draft_id: str = "",
name: str = "",
file_path: str = "",
planning_notes: str = "",
) -> dict:
"""
Create a workflow draft or replace an existing draft.
Expand All @@ -100,9 +107,9 @@ async def create_workflow(
user_id=user_id,
definition=workflow,
turn_id=turn_id,
draft_id=draft_id,
file_path=file_path,
name=name,
draft_id=_empty_to_none(draft_id),
file_path=_empty_to_none(file_path),
name=_empty_to_none(name),
)
return {"status": "success", "draft_id": saved.draft_id}

Expand All @@ -111,15 +118,19 @@ async def create_workflow(

def create_read_workflow_tool(session_id: str) -> callable:
@tool
async def read_workflow(draft_id: str | None = None, file_path: str | None = None) -> dict:
async def read_workflow(draft_id: str = "", file_path: str = "") -> dict:
"""
Read an existing workflow draft.

Args:
draft_id: Workflow draft id. Preferred.
file_path: Explicit legacy sandbox workflow JSON file path. Fallback only.
"""
result = await read_workflow_definition(session_id=session_id, draft_id=draft_id, file_path=file_path)
result = await read_workflow_definition(
session_id=session_id,
draft_id=_empty_to_none(draft_id),
file_path=_empty_to_none(file_path),
)
return result.to_tool_response()

return read_workflow
Expand All @@ -134,10 +145,10 @@ def create_update_workflow_tool(
@tool
async def update_workflow(
workflow: dict,
draft_id: str | None = None,
name: str | None = None,
file_path: str | None = None,
planning_notes: str | None = None,
draft_id: str = "",
name: str = "",
file_path: str = "",
planning_notes: str = "",
) -> dict:
"""
Update an existing workflow draft or overwrite a file-backed workflow.
Expand All @@ -153,7 +164,7 @@ async def update_workflow(
blocked = _guard_repair_limit(repair_state)
if blocked:
return blocked
reuse_failure = _require_reuse_after_failure(repair_state, draft_id)
reuse_failure = _require_reuse_after_failure(repair_state, _empty_to_none(draft_id))
if reuse_failure:
return reuse_failure
workflow = _normalize_workflow_payload_shape(workflow)
Expand All @@ -162,9 +173,9 @@ async def update_workflow(
user_id=user_id,
definition=workflow,
turn_id=turn_id,
draft_id=draft_id,
file_path=file_path,
name=name,
draft_id=_empty_to_none(draft_id),
file_path=_empty_to_none(file_path),
name=_empty_to_none(name),
)
return {"status": "success", "draft_id": saved.draft_id}

Expand Down Expand Up @@ -228,10 +239,10 @@ def create_workflow_and_run_tool(
@tool
async def create_workflow_and_run(
workflow: dict,
draft_id: str | None = None,
name: str | None = None,
file_path: str | None = None,
planning_notes: str | None = None,
draft_id: str = "",
name: str = "",
file_path: str = "",
planning_notes: str = "",
) -> dict:
"""
Create or update a workflow draft and run it immediately.
Expand All @@ -247,23 +258,23 @@ async def create_workflow_and_run(
blocked = _guard_repair_limit(repair_state)
if blocked:
return blocked
reuse_failure = _require_reuse_after_failure(repair_state, draft_id)
reuse_failure = _require_reuse_after_failure(repair_state, _empty_to_none(draft_id))
if reuse_failure:
return reuse_failure
workflow = _normalize_workflow_payload_shape(workflow)
outcome = await create_and_run_agent_workflow_draft(
session_id=session_id,
definition=workflow,
turn_id=turn_id,
draft_id=draft_id,
file_path=file_path,
name=name,
draft_id=_empty_to_none(draft_id),
file_path=_empty_to_none(file_path),
name=_empty_to_none(name),
)
if outcome.is_failure:
return _build_run_failure_response(outcome, repair_state)
normalized = _normalize_workflow_run_result(
outcome.raw_result or {},
draft_id=outcome.draft_id or draft_id,
draft_id=outcome.draft_id or _empty_to_none(draft_id),
workflow_definition=outcome.workflow_definition or workflow,
)
return _finalize_workflow_run_result(normalized, repair_state, outcome.draft_id or draft_id or "")
Expand Down
Loading
Loading