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.