Agent Runner — Batch File Processing

UML Sequence  ·  Electron FE ↔ FastAPI BE ↔ LLM

sequenceDiagram
    autonumber

    participant FE as Electron FE
(AgentScheduler · DrizzleExecutor) participant BE as FastAPI BE
(AgentRunner · routes/agents) participant LLM as LLM
(gpt-4.1 via LiteLLM) note over FE: AgentScheduler cron tick
or manual "Run Now" rect rgb(30, 33, 48) note right of FE: PHASE 1 — TRIGGER FE->>+BE: POST /agents/trigger
(agent config, directories, schedule) BE->>BE: billing check · concurrency guard
create AgentRunLog (status=running) BE-->>-FE: 202 Accepted { run_id } note over FE: store runId in local agentRuns note over BE: asyncio.create_task(run_local_agent)
fire-and-forget background task end rect rgb(25, 37, 35) note right of FE: PHASE 2 — DIRECTORY SCAN (via WS → FE filesystem) loop For each directory in agent config (max depth=5) BE->>FE: WS tool_call: list_directory { path } FE->>FE: fs.readdir(path) FE-->>BE: WS tool_result: { entries[] } BE->>FE: WS tool_call: get_file_metadata { path } FE->>FE: fs.stat(file) FE-->>BE: WS tool_result: { modifiedAt, size } note over BE: Skip if modifiedAt ≤ last_run_at
(first run: last_run_at=null → all pass) end note over BE: Result: file_list[] — paths passing
extension + date filters (e.g., 22 files) end BE->>FE: WS tool_call: select { table: "projects" } FE-->>BE: WS tool_result: { rows[] } note over BE: Cache project list for prompt context rect rgb(35, 30, 22) note right of FE: PHASE 3+4 — FOR EACH FILE: READ → PREPROCESS → LLM loop For each file in file_list (sequential) rect rgb(40, 35, 25) note over BE: Phase 3 — Read + Preprocess BE->>FE: WS tool_call: read_file_content { path } FE->>FE: fs.readFile(path) FE-->>BE: WS tool_result: { content } BE->>BE: detect_content_type(filename, content)
preprocess() → clean text + metadata
(Python only, zero LLM calls) end rect rgb(30, 28, 42) note over BE: Phase 4 — LLM Agent Tool Loop BE->>+LLM: system prompt + clean text
+ available tools + project list loop Max 12 tool-call iterations LLM-->>BE: tool_call (e.g., list_tasks, create_note) BE->>FE: WS tool_call: select/insert/update
{ table, data } FE->>FE: DrizzleExecutor
local SQLite CRUD FE-->>BE: WS tool_result: { rows } BE->>LLM: tool_result → continue end LLM-->>-BE: final text response (no more tool_calls) end note over BE: log: file processed, created=N entities opt If create_project was called BE->>FE: WS tool_call: select { table: "projects" } FE-->>BE: WS tool_result: { rows[] } note over BE: Refresh project list cache end end end rect rgb(22, 35, 32) note right of FE: PHASE 5 — COMPLETION BE->>BE: _finalize_run()
update AgentRunLog in PostgreSQL
status = success | partial | error BE->>FE: WS run_complete: { run_id, status } FE->>FE: update local agentRuns table
{ status, completedAt } end note over FE,LLM: ⚠ No file-path journal exists. On re-trigger,
BE re-scans all files. Date filter (modifiedAt > last_run_at)
skips unchanged files, but LLM dedup is the only guard
if last_run_at is null or files are unmodified.