Skip to content

Scheduler module: LLM-generated timeboxed schedule on /done #5

Description

@cristoforows

Parent

#2 (PRD: /timebox — LLM-assisted next-day timeboxing)

What to build

Replace the /done echo stub from #4 with real schedule generation, implemented as a deep, independently-testable scheduler module. Read the PRD (#2) Implementation Decisions section first — all design decisions are settled there.

The scheduler module owns three responsibilities behind a small interface:

  1. Target-date computation (pure function): given the current datetime, a timezone, and a cutoff hour → the date being planned. Normally tomorrow in the configured timezone; if local time is between 00:00 and 02:59 (hours strictly below the cutoff), the current day instead. Use the stdlib zoneinfo.
  2. Schedule generation: given the raw task messages (verbatim, order preserved), the target date, and an injected LLM client → a structured result. The LLM parses each message into a task with optional duration/constraints (e.g. "deep work 90m morning", "call mom after 6pm"), estimates missing durations, honors freeform constraints including waking-window overrides stated by the user (e.g. "start my day at 7"), and produces a non-overlapping plan inside the default 09:00–22:00 window. If everything cannot fit, the LLM unilaterally drops the least important / least time-pressured tasks and gives a one-line reason per drop. Result contract: a schedule list of {start HH:MM, end HH:MM, task, optional note} plus a dropped list of {task, reason}. Enforce the contract with a Pydantic schema via langchain's .with_structured_output().
  3. Rendering (pure function): structured result → formatted Telegram text — time ranges + task names, then a "Dropped" section with reasons; omit the Dropped section when empty.

LLM plumbing: OpenRouter through langchain-openai's ChatOpenAI with base_url https://openrouter.ai/api/v1 — mirror the pattern in the sibling second_brain_summarizer repo (src/second_brain/agent/llm.py). New deps: langchain>=0.3,<0.4 and langchain-openai>=0.3,<0.4.

Config additions, following the existing Config class pattern (env getter + default + warning): OPENROUTER_API_KEY (required — fail fast at startup with a clear error if missing), TIMEBOX_LLM_MODEL (default deepseek/deepseek-v4-flash), TIMEBOX_TIMEZONE (default Asia/Singapore), TIMEBOX_CUTOFF_HOUR (default 3). Keep TIMEBOX_CUTOFF_HOUR separate from the existing DAY_CUTOFF_HOUR — do not couple them.

Testing (first tests in this repo — introduce pytest and a tests/ directory): cover the scheduler module only, asserting external behavior. Target-date computation around the cutoff and across timezone boundaries (server-UTC vs Asia/Singapore disagreeing on "today"); rendering with and without dropped items; generation happy path with a fake injected LLM. Do not test the Telegram handler.

Keep the prompt in one obvious place in the module — it will be tuned after real use.

Acceptance criteria

  • /done with collected tasks returns a formatted timeboxed schedule for the correct target date
  • Session at 23:30 local plans tomorrow; session at 01:30 local plans the current day
  • Tasks with explicit durations/constraints are respected; tasks without get LLM-estimated durations
  • Schedule slots are non-overlapping and inside 09:00–22:00 unless the user stated a different window in a task message
  • When tasks exceed the day, the reply includes a Dropped section listing each cut task with a reason
  • Missing OPENROUTER_API_KEY aborts startup with a clear error message
  • Model, timezone, and cutoff hour are configurable via env vars with the documented defaults
  • pytest suite passes offline (fake LLM, no network) covering target-date computation, rendering, and generation happy path

Blocked by

Metadata

Metadata

Assignees

No one assigned

    Labels

    ready-for-agentTriage: ready for an AFK agent to pick up

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions