mirror of
https://github.com/bytedance/deer-flow.git
synced 2026-08-01 19:06:01 +00:00
8 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
f68fb0c4cf |
docs(schedule): lead the walkthrough with a behavioural spec
Restructure per review: state what the system promises before how the code delivers it. The thin product-decision table becomes a full §1 behavioural spec written in zero implementation vocabulary -- user operations, scheduling semantics, the skip-not-queue overlap decision, the busy-wait guarantees (delayed, never lost; ordered by due time; no catch-up of missed cron periods), the failure/recovery table, and an explicit non-goals list (no queue, no punctuality guarantee, no task credentials, single scheduler instance) -- each promise cross-referenced to the section whose mechanism delivers it. The mechanism chapters absorb what the spec now promises: §6.2 explains where waiting goes (the table as an implicit priority queue via the claim query's ordering), §6.3 and a new pitfall entry name the budget-leak boundary when a completion callback is lost, §8's defence table gains the race-window column and a per-layer error-translation table as the journey's unhappy half, and the HTTP endpoint block moves beside the commands it carries. |
||
|
|
563245d96f |
docs(schedule): rewrite the module walkthrough for the finished slice
Bring SCHEDULE_DESIGN_zh.md to the same reference-implementation walkthrough shape as FEEDBACK_DESIGN_zh.md: it now assumes the spec has been read, drops the mid-migration 'current state' warnings (the legacy code is deleted), and covers what the old version predated -- the commands chapter (UNSET three-state updates, ContextChange, why the clock- and callback-driven writes stay plain methods), the adapters and composition-root chapter (CAS field ownership, the corrupt-row error split, the two anti-corruption layers and the inbound completion listener), the dispatch journey with the three-driver macro diagram the spec's §5.3 points at, and the test-layering table for the current suites. The spec's §6 marks the schedule slice done and drops the three completed to-dos; the docs index gains the schedule walkthrough next to its sibling. |
||
|
|
34ce7c259b |
refactor(schedule): make the run-completion hook an inbound adapter
`run_outcome_mapping.py` called itself "not a port implementation" and sat in a package of secondary adapters, while the half that actually invoked the use case lived as a closure in the composition root. It is one thing, and it is a primary adapter: the run runtime calls it the way HTTP calls the router and the clock calls the poller. `ScheduleRunCompletionListener` now holds the whole responsibility -- decide whether a finished run is ours, translate it, invoke the use case. Those are not two jobs: "ignore this run" is only meaningful as "do not call the service", so splitting them is what left the second half in a place where behaviour is not asserted. `build_run_completion_hook` drops to `return ScheduleRunCompletionListener(service)`. The composition root's own docstring says no adapter logic lives there; that is now true of it as well as of the routers it was written about. Placement --------- Kept in `app/adapters/schedule/` rather than moved beside the other two primary adapters. The context stays in one package; direction is stated by the class name and each module's first line, and the package `__init__` -- previously empty -- now lists which of its modules point which way, so a file added without that line is visibly a file whose direction nobody decided. A subdirectory for a single inbound module would have made the other four look like they had been sorted into something. Tests ----- This is the part that was not a rename. The conversion had 24 cases; the invocation had none, because the composition root is not where behaviour is asserted, so nothing covered "an ordinary chat run must not reach the service" as opposed to "produces no outcome object". The cases now drive `__call__` against a recording service, which asserts the same mappings plus what was done with them, and adds the two that were unreachable before: the service left entirely alone for a filtered run, and the completion stamped with a tz-aware current instant. `test_composition.py` gains `TestRunCompletionHook` for the assembly decision that remains -- including that the hook is bound to the service it was given, which a wrong wiring would type-check past. Confirmed by mutation that this case fails when the binding is broken. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
680f044e58 |
refactor(schedule): move schedule_spec translation onto its owners
`spec_column.py` and `spec_wire.py` were the same 60 lines twice, kept in sync by a parity suite. Both are gone; what they did is now split along the line that actually separates the two boundaries. Why two files existed --------------------- They were one module until the slice split it, and the reason given for the split holds: a primary adapter must not import a secondary one, and the two shapes are equal only by coincidence. But that argument only requires the two *shapes* to be independent -- it does not require the parsing *rule* to be written twice, and writing it twice is what needed `test_schedule_spec_parity` to assert the two agreed, down to identical error text. Splitting it properly --------------------- `ScheduleSpec.from_primitives(schedule_type, *, cron, run_at, timezone)` takes four strings, not a `Mapping[str, Any]` -- the mapping was the thing that kept this out of the domain, and four strings carry no transport or storage format with them. It owns the whole rule: unknown type, missing or non-string field, unparseable `run_at`, and (via __post_init__, unchanged) 5-field cron and resolvable timezone. Values are checked rather than trusted, since both callers read data a client can influence. Each adapter keeps only what is genuinely its own -- which two keys its format uses -- as private methods on the class that owns the boundary, matching how `SqlFeedbackRepository` and AWS's own ports-and-adapters sample put the conversion inside the adapter rather than beside it: SqlScheduledTaskRepository._spec_from_row / _spec_to_column models.ScheduledTaskCreateRequest.to_schedule / models._spec_to_wire The emit direction stays duplicated, deliberately: it is three lines per side with no rule in it, and the two are *allowed* to diverge -- one is an HTTP contract, the other a storage format. Asserting they stay byte-identical was a constraint neither side asked for, so that suite is not replaced. The router stops building value objects --------------------------------------- `create` passes `body.to_schedule()`. `update` passes `body.to_schedule(current.schedule)`, replacing eight lines that re-emitted the current spec to the wire shape purely to read defaults back out of it; omitted parts now come off the value object directly. Tests ----- `test_schedule_spec_parity.py` is deleted (162 lines). Its structural and value cases moved to `TestFromPrimitives` in the domain suite -- stated once now instead of parametrized over two implementations. Emit coverage was already elsewhere: wire in `test_schedule_response_models`, column via the repository round-trip in `test_schedule_fakes`. One gap found while removing it: the `once` half of the update fallback had no coverage on either side (both existing cases use cron), and it is exactly the branch this commit rewrites -- `spec_to_wire(current)` round-trip to `current.run_at.isoformat()`. Added `test_a_timezone_change_on_a_once_task_keeps_the_same_instant`, and confirmed by mutation that it is the only case that catches that branch breaking. Behaviour is unchanged: same wire shapes, same normalizations (whitespace in cron, trailing Z re-emitted as +00:00), same error messages, so the 422 details clients see do not move. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
eb37df0779 |
refactor(schedule): wire the slice through a composition root
Switches production onto the hexagonal path. The legacy modules still compile and still have tests, but nothing assembles them any more; deleting them is the next commit, kept separate so it stays reviewable. Composition root ---------------- `app/composition.py::build_domain_services()` is now the only place an adapter is instantiated. It was extracted from `deps.py::langgraph_runtime` rather than added to it: wiring there was tangled with engine startup, orphan recovery and shutdown, so the one rule that governs it -- no SQL backend means no service and the routes answer 503 -- could not be tested without booting the whole application, and was held up by a single comment. It is a pure function of already-built infrastructure, so that rule is now an assertion. Feedback moved with it; doing this while adding schedule's five objects costs one change instead of two. Primary adapter --------------- The router is protocol translation only. What is gone is the giveaway: cron normalisation, `next_run_at` arithmetic, the re-arm rule and hand-written ownership checks all now live in the aggregate. Domain errors map to status codes through one table, so a new error surfaces as a 500 to be classified rather than being swallowed by whichever `except` was nearest. `spec_mapping` split in two (AWS's own layout puts the wire model under the entrypoint that owns it, and a primary adapter must not import a secondary one): `adapters/schedule/spec_column.py` for the JSON column, `routers/schedule/spec_wire.py` for the HTTP body. The two shapes are equal only by coincidence, so `test_schedule_spec_parity.py` runs every case against both and compares their outputs and messages directly. Function names differ per side so an import from the wrong one is visible. Explicit responses ------------------ Routes returned the ORM row's `to_dict()`, leaking `user_id`, `assistant_id`, `overlap_policy` and the two lease columns. The response models publish exactly the field set the frontend declares -- asserted in both directions, since an extra field is a leak and a missing one breaks a client. One wire detail was nearly changed by accident: Pydantic v2 serializes a UTC datetime as `...Z`, while the legacy `coerce_iso` path emitted `+00:00`. `UtcTimestamp` pins `isoformat()` so adopting a model does not silently alter the wire format for every client parsing these. Tests ----- 73 new cases: router behaviour driven through a real `ScheduleService` over in-memory fakes (a mocked service would let the error mapping pass without a domain error ever being raised), response shape, and the composition root. Router mappings verified by mutation -- a wrong status code or a dropped timezone fallback turns 9, 2 and 1 cases red respectively. Two lifespan tests carried a `SimpleNamespace` config that predates this change; `langgraph_runtime` now reads `config.scheduler`, so they were given one. Tolerating the gap with `getattr` was rejected: `AppConfig.scheduler` always exists, so the fallback would be unreachable in production and exist purely to excuse an incomplete test double. Full suite is back to its 24 pre-existing failures. |
||
|
|
0bae77ffc0 |
refactor(schedule): move the secondary adapters to app/adapters
Adopts the layout feedback landed in cb49dd67: secondary adapters live under `app/adapters/<context>/`, one file per port, the file named after the port in snake_case with the technology carried by the class name. `app/infra/` is now gone entirely. The rename also separates two meanings of "run" that shared one filename space: `run_sql.py` held `ScheduledRun` (an execution record), while the `run_launcher.py` still to come deals in Gateway runs. task_sql.py -> scheduled_task_repository.py run_sql.py -> scheduled_run_repository.py spec_mapping.py -> spec_mapping.py (implements no port, so no rename) Both SQL adapters now carry the `Secondary adapter (owned persistence)` docstring marker -- this context owns both tables and writes its own queries. `spec_mapping` says instead that it is a boundary mapping and names its two callers. The package `__init__.py` is empty, so imports go through the full path and a class's home file stays unambiguous. Pure move: every top-level symbol was compared against its pre-move original by AST dump (docstrings excluded), plus a separate per-class method-name comparison, since a whole-class dump reports a docstring edit and a renamed method the same way. Also repoints three docstrings and one diagram that still named the deleted `app/infra/` path. |
||
|
|
b56e939c4b |
docs(schedule): cover the ports and the application service
The design doc stopped at the model layer while ports.py and service.py had already landed, so it described the inner ring as one third of what is actually there and still called the other two "to be built". Two new chapters. Ports covers the three contracts that matter more than the signatures — another user's task reads as absent rather than forbidden, only ThreadBusyError or LaunchFailedError may escape a launch, and RunOutcome keeps the run runtime out of the domain — plus the two deliberate absences (no Clock, no claimer identity) and the line between single-threaded semantics, which the contract owns, and atomicity, which it does not. The service chapter walks dispatch_task's four exits as a diagram, explains why the global budget is not a per-poll batch size, and records why the context change travels packaged rather than behind a sentinel. The rest follows: the migration map now shows the inner ring complete and only adapters outstanding, the overview gains the discipline each of the three layers is held to, the dev guide gains "add a use case" and a warning against writing rules in the service, and the pitfalls and glossary pick up what the new layers introduce. |
||
|
|
9d0b09558e |
feat(schedule): add the schedule domain model
First step of the scheduled-task context's hexagonal migration: the inner-ring model, with zero infrastructure dependencies. - ScheduleSpec / SchedulePolicy value objects - ScheduledTask aggregate root - ScheduledRun aggregate - 9 domain errors, 5 enums Rules are migrated verbatim from their current homes, each method's docstring citing the source line: timezone/cron/next-run calculation from deerflow/scheduler/schedules.py, context-mode and re-arm rules from routers/scheduled_tasks.py, and the four status-derivation rules from app/scheduler/service.py. Two things previously held by convention are now enforced by construction. Validation and normalization live in __post_init__, so building a ScheduleSpec field-by-field cannot bypass them. The skipped tombstone is a separate factory, so it can never be written as the transient queued row that would collide with uq_scheduled_task_run_active. The domain does not serialize itself: mapping the stored schedule_spec JSON in and out stays with the adapter layer, keeping Mapping[str, Any] out of every domain signature. Production code still runs through app/scheduler/service.py -- this commit adds no call sites and changes no behavior. |