mirror of
https://github.com/bytedance/deer-flow.git
synced 2026-10-26 21:26:39 +00:00
fix(scheduled-tasks): honor explicitly null assistant_id (#6185)
Co-authored-by: lakers-abing <236384465+lakers-abing@users.noreply.github.com>
This commit is contained in:
parent
9ff47cfae5
commit
fafe904106
@ -2249,6 +2249,7 @@ Current MVP capabilities:
|
||||
- One-time task forms reject local times skipped by daylight-saving transitions; select another time before creating or saving the task.
|
||||
- Choose whether each scheduled task reuses a thread and its conversation history or creates a fresh thread per run
|
||||
- Pin each task to `lead_agent` (default) or a custom agent the owner already has; unknown names are rejected
|
||||
- Sending `assistant_id: null` in a scheduled-task PATCH resets the task to `lead_agent`; omitting `assistant_id` preserves the current agent, including when that custom agent has since been deleted
|
||||
- Duplicate an existing task into the create form as an editable draft without copying its run history
|
||||
- Support `once`, `cron`, and `interval` schedules
|
||||
- Editing or duplicating an interval task preserves its saved cadence until the interval is explicitly changed, including sub-minute intervals allowed by the operator's scheduler configuration
|
||||
|
||||
@ -207,7 +207,7 @@ startup gate rejects process-local memory and JSONL event stores when
|
||||
- Thread-scoped Gateway runs evaluate an active `ThreadState.goal` after the visible turn completes. `runtime/goal.py` asks a non-thinking evaluator model to judge only visible conversation evidence and return a typed blocker; the evaluator model is created once per run and reused across hidden continuation checks. The evaluator runs after the graph root's tracing scope has already closed, so `create_goal_evaluator_model`/`evaluate_goal_completion` attach their own model-level tracing callbacks (`attach_tracing=True`) and inject Langfuse trace metadata (`thread_id`/`user_id`/`deerflow_trace_id`) directly onto the `ainvoke` call — the same standalone-caller pattern as `oneshot_llm.run_oneshot_llm` and `MemoryUpdater` (see Tracing System below). Satisfied goals are cleared; every non-satisfied evaluation — continuable or stand-down — is persisted with `last_evaluation` (the blocker, reason, and evidence summary; outcomes that stop the loop additionally record a `stand_down_reason` for observability), but only `goal_not_met_yet` evaluations are streamed as hidden `HumanMessage` continuations, and only when the turn did not end on an unanswered Human Input Card (`ask_clarification` or the sandbox network prompt; the question is still open, so the goal stands down with `blocker: "needs_user_input"` without calling the evaluator), a durable assistant end-of-turn checkpoint exists, the run has not been aborted, the run has not hit its token-budget hard stop (`stop_reason == "token_capped"`; the goal stands down with `stand_down_reason: "token_capped"`), the thread did not change during evaluation, and the no-progress breaker has not fired. The continuation cap is 8 — a hard maximum in the `0`–`8` range; callers requesting more are clamped (`set_goal`/TUI) or rejected with 422 (`PUT /goal`). The no-progress breaker keys on the latest visible assistant evidence (not the evaluator's free-text reason, which an LLM rewords every turn), so two consecutive continuations that add no new visible assistant output stop the loop after 2 attempts. Model-response cleanup helpers such as think-block stripping and code-fence stripping live in `deerflow.utils.llm_text` so `runtime/goal.py` and Gateway suggestion parsing share the same JSON-prep behavior.
|
||||
- Run event stream changes must keep producer code, `deerflow/constants.py`, `runtime/events/catalog.py`, `contracts/run_event_stream_contract.json`, `backend/docs/RUN_EVENT_STREAM.md`, and `tests/test_run_event_stream_contract.py` in sync. The dependency-free constants module owns the persisted envelope limits (`event_type` 32 characters, `category` 16) and cross-layer workspace event identity; the catalog owns validated runtime definitions and categories. Dynamic middleware tags are limited to 21 characters after the `middleware:` prefix. The JSON contract owns payload schemas, backend-specific storage semantics, legacy aliases, and compatibility rules; conformance tests require both views and all producer groups to agree. `run.end.content` remains opaque and may retain nested Python values in memory while JSONL/database stores stringify non-JSON nested values, so consumers must not assume backend-identical nested output representations.
|
||||
|
||||
Proxied through nginx: `/api/langgraph/*` → Gateway LangGraph-compatible runtime, all other `/api/*` → Gateway REST APIs.
|
||||
Scheduled-task PATCH: omitted `assistant_id` preserves the pin; explicit null resets to `lead_agent`.
|
||||
|
||||
**Thread lifecycle**: Before changing branching, regeneration, edit replay, or
|
||||
archive/search behavior, read [Thread lifecycle invariants](../../docs/THREAD_LIFECYCLE.md).
|
||||
|
||||
@ -277,9 +277,9 @@ async def update_scheduled_task(task_id: str, request: Request, body: ScheduledT
|
||||
await _ensure_task_mutable(existing, repo)
|
||||
|
||||
updates = body.model_dump(exclude_none=True)
|
||||
if "assistant_id" in updates:
|
||||
if "assistant_id" in body.model_fields_set:
|
||||
updates["assistant_id"] = await resolve_scheduled_task_assistant_id(
|
||||
updates["assistant_id"],
|
||||
body.assistant_id,
|
||||
user_id=str(user.id),
|
||||
)
|
||||
if "context_mode" in updates:
|
||||
|
||||
@ -1076,7 +1076,7 @@ def _create_request(**overrides):
|
||||
return scheduled_tasks.ScheduledTaskCreateRequest(**kwargs)
|
||||
|
||||
|
||||
async def _seed_task(repo: _Repo, **overrides):
|
||||
async def _seed_task(repo: _Repo | ScheduledTaskRepository, **overrides):
|
||||
kwargs = {
|
||||
"task_id": "task-1",
|
||||
"user_id": "user-1",
|
||||
@ -1354,6 +1354,52 @@ async def test_update_invalid_assistant_id_is_rejected():
|
||||
assert "Invalid assistant_id" in exc_info.value.detail
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_update_explicit_null_assistant_id_resets_to_lead_agent():
|
||||
repo = _Repo()
|
||||
task = await _seed_task(repo, assistant_id="research-bot")
|
||||
with patch(
|
||||
"app.gateway.routers.scheduled_tasks.load_agent_config",
|
||||
side_effect=FileNotFoundError("missing"),
|
||||
) as loader:
|
||||
updated = await _call_update(
|
||||
repo,
|
||||
task["id"],
|
||||
scheduled_tasks.ScheduledTaskUpdateRequest(assistant_id=None),
|
||||
)
|
||||
loader.assert_not_called()
|
||||
assert updated["assistant_id"] == "lead_agent"
|
||||
assert repo.items[task["id"]]["assistant_id"] == "lead_agent"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.parametrize("payload,expected_assistant", [({"assistant_id": None}, "lead_agent"), ({}, "research-bot")], ids=["explicit-null", "omitted"])
|
||||
async def test_update_assistant_id_through_the_sql_repository(tmp_path, payload, expected_assistant):
|
||||
await init_engine_from_config(DatabaseConfig(backend="sqlite", sqlite_dir=str(tmp_path)))
|
||||
try:
|
||||
sf = get_session_factory()
|
||||
assert sf is not None
|
||||
repo = ScheduledTaskRepository(sf)
|
||||
task = await _seed_task(repo, assistant_id="research-bot")
|
||||
with patch(
|
||||
"app.gateway.routers.scheduled_tasks.load_agent_config",
|
||||
side_effect=FileNotFoundError("missing"),
|
||||
) as loader:
|
||||
updated = await _call_update(
|
||||
repo,
|
||||
task["id"],
|
||||
scheduled_tasks.ScheduledTaskUpdateRequest.model_validate({"title": "Renamed", **payload}),
|
||||
)
|
||||
loader.assert_not_called()
|
||||
assert updated["assistant_id"] == expected_assistant
|
||||
stored = await repo.get(task["id"], user_id="user-1")
|
||||
assert stored is not None
|
||||
assert stored["assistant_id"] == expected_assistant
|
||||
assert stored["title"] == "Renamed"
|
||||
finally:
|
||||
await close_engine()
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_update_omitting_assistant_id_keeps_existing_even_if_agent_is_gone():
|
||||
# Unrelated PATCH (rename, reschedule) must not re-resolve assistant_id.
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user