feat(ui): scheduled recurring tasks in Agent UI#1566
Conversation
Ported from kalin/autonomous-agent-infra (#517) with fixes: - ScheduleConfig.from_json raises on malformed/unknown-key JSON instead of silently returning a config that never fires - Scheduler requires a callable executor (dry-run no-op path removed) - _load_tasks propagates DB errors instead of booting with zero tasks; corrupt schedule_config rows fail the boot loudly - All SQL moved behind public ChatDatabase methods (no _lock/_conn use) - Concurrency semaphore (default 1) bounds simultaneous task runs - Session-write failures log at ERROR with tracebacks
Scheduled runs are suspended while a public tunnel is active (same posture as AgentLoop) unless GAIA_AUTONOMOUS_ALLOW_TUNNEL is set. Per-run timeout configurable via GAIA_SCHEDULE_TIMEOUT (default 300s).
Clock button in the sidebar opens the schedule panel: natural-language schedule input with live parse preview, create/pause/resume/delete with confirm, run counts and next-run countdown.
Adapted from #517: real ChatDatabase instead of a private-API fake, explicit executors everywhere. Dropped TestServerSchedulerWiring — it guarded against the silent dry-run mode, which no longer exists (the Scheduler now raises at construction without an executor).
Review: Scheduled recurring tasks in Agent UISolid, well-tested feature that lands cleanly on current architecture — persistence, fail-loudly config handling, a concurrency cap, and the tunnel gate are all here, backed by 85+ tests across unit/API/integration tiers. No security or breaking-change concerns. The one thing worth addressing before merge: the tunnel "suspend" path doesn't suspend — it fails every run and inflates Issues🟡 Tunnel-active runs fail loudly instead of suspending ( The executor raises Recommend gating in the run loop before counting a run — skip and re-sleep rather than execute-and-fail. Sketch: # in _run_loop, before await self._execute_task(task):
if self._tunnel_blocked(): # inject the same tunnel check the server uses
logger.info("Schedule '%s' skipped: tunnel active", task.name)
await asyncio.sleep(min(task.interval_seconds, 60))
continueThat keeps the security guarantee (LLM never runs) without the error-count/history noise. The tunnel check currently lives in the server closure; passing a small predicate into 🟡 No minimum-interval floor; fresh
Minor🟢 Unused imports in the integration test ( (drop the 🟢 Inaccurate type hint ( 🟢 🟢 Timing-based tests ( Strengths
VerdictApprove with suggestions. No blocking correctness or security issues; the feature works and is well-covered. Please address the tunnel-suspend behavior (or at least reconcile the docs with what actually happens) — the rest are quick cleanups. |
|
🟡
The other test files in this PR ( @pytest_asyncio.fixture
async def app_with_scheduler():
db = ChatDatabase(":memory:")
scheduler = Scheduler(db=db, executor=fake_executor)
await scheduler.start()
app = FastAPI()
app.include_router(router)
app.state.scheduler = scheduler
app.dependency_overrides[get_scheduler] = lambda: scheduler
yield app, scheduler, db
await scheduler.shutdown()
db.close()And replace |
Drop unused dataclasses.field import in scheduler.py and unused pytest_asyncio/ScheduledTask/parse_interval imports in the scheduler e2e test. Resolves the Pylint W0611 / Flake8 F401 failures.
|
🟡 The Teardown then calls task._timer_task.cancel()
try:
await task._timer_task # ← Task lives on a different loop → ValueError
except asyncio.CancelledError:
passOnly Fix: use a pytest-asyncio async fixture so everything shares one loop: @pytest_asyncio.fixture
async def app_with_scheduler():
db = ChatDatabase(":memory:")
scheduler = Scheduler(db=db, executor=fake_executor)
await scheduler.start()
app = FastAPI()
app.include_router(router)
app.dependency_overrides[get_scheduler] = lambda: scheduler
yield app, scheduler, db
await scheduler.shutdown()
db.close()The tests would then use |
|
🟡
|
Prepare the schedule store to converge with the Agent UI's SQLite store (amd#1566) and close the test/doc gaps the initial CLI PR deferred. - Schedule gains last_run/next_run/session_ref so one dataclass serves both the hand-editable TOML layer and the future SQLite/run-history store; TOML serialization omits each field when unset. - ScheduleStore becomes a runtime_checkable Protocol (load/save/add/remove/get/set_enabled/mark_run) and the TOML implementation is renamed TomlScheduleStore, so a database-backed store can drop in without changing the daemon or CLI. - mark_run records last_run/next_run after a successful fire (daemon and `gaia schedule run`); a failed fire still logs loudly and leaves run-state untouched, keeping the daemon alive. - build_scheduler now depends only on the Protocol surface (no concrete store.path access) so the UI server can embed the same engine on a background thread instead of requiring a separate daemon process. - Add hermetic unit tests for the store, sinks, and daemon (LLM and network mocked), and document `gaia schedule` in docs/reference/cli.mdx.
## Why this matters The v2 sidecar-first architecture (amd#1913) is signed-in design, but nothing yet tells an implementer where to start, what's reusable vs. net-new, or which unsigned decisions block which work — so the build can't be parallelized or even safely begun. This adds the implementation decomposition: a code-verified delta table for every v2 building block, 19 sequenced issue proposals (with acceptance criteria, dependencies, S/M/L sizes — proposals only, none filed), a specced first vertical slice (headless daemon + supervised email sidecar + `/query` streamed to the CLI), and the three sign-off decisions (§0.4 confirmation model, §0.9 custody home, §0.24 containment 🔒) as explicit questions. It also pins verified design-vs-merged deviations so implementers don't trip on them: `gaia api` still mounts email in-process (the pattern amd#1910 removed from the UI only), the only scheduler on main is UI-internal (PR amd#1566 — `gaia schedule`/PR amd#1371 is unmerged), and contract 2.1 has no `/query`, no auth (amd#1706), and no `/shutdown`. ## Test plan - [ ] Docs-only — no code paths touched - [ ] Spot-check the delta table's file/LoC claims against `main` @ `080e7259` (all were verified while writing) - [ ] Confirm the issue proposals don't duplicate existing open issues (checked: amd#1706, amd#1653, amd#1896, amd#1717 are linked, not duplicated)
Why this matters
GAIA had no way to run anything on a schedule from the Agent UI — every recurring use case ("every morning at 9, summarize my inbox") needed external tooling. This adds a Schedule Manager to the UI: describe a schedule in natural language ("every 30m", "daily at 9pm", "every monday at 8am"), attach a prompt, and the agent runs it on time through the local LLM, with each schedule's full run history living in its own chat session. Salvaged from #517 and rebuilt on current main with fail-loudly hardening (malformed configs raise instead of silently never firing; the scheduler refuses to boot with lost tasks; no dry-run no-op path), a concurrency cap, and the same tunnel security gate the autonomous agent loop uses.
Scope notes: NL intervals only — cron syntax and heartbeat tiers stay with the autonomy-engine plan, and the
gaia scheduleCLI (#1371) remains a separate complementary surface (coordination comment posted there; store unification is a follow-up for whichever lands second).Test plan
tests/unit/test_scheduler.py)tests/unit/test_scheduler_api.py)tests/integration/test_scheduler_e2e.py)tsc --noEmitclean + frontend build/api/schedulesCRUD round-trip on a live server