Envoy Orchestrator Implementation

Complete implementation of the Envoy email-driven AI agent orchestrator with OpenAI integration and structured outputs.

Status

✓ Implemented and tested end-to-end (2026-02-12)✓ Continuation emails working (2026-02-13)✓ Multi-mailbox support (2026-02-13)✓ Single IMAP connection architecture (2026-02-13)✓ Email delivery dispatch table with WebDAV support (2026-02-14)✓ Phase-aware processing state machine — 9 phases (2026-02-22)✓ dispatch_immediate_actions() — write/delete notes + send emails on any phase (2026-02-22)✓ Context bundles via bundle_key — auto-loaded each iteration (2026-02-22)✓ Per-state instruction notes — envoy/states/{phase} auto-loaded (2026-02-22)✓ Double-send prevention via sent_to_addresses tracking (2026-02-22)✓ Multi-folder move-email search — INBOX then Active (2026-02-22)✓ Sent IMAP copy of continuation emails for test resume (2026-02-22)✓ Test harness test_orchestrator.py (2026-02-22)

Core Components

orchestrator.py

Main processing loop that coordinates all operations:

• Opens a single IMAP connection in main(), passed to all functions• Expunges \Deleted messages at startup• Fetches folder list and injects into LLM context on iteration 1• Fetches emails from configurable folder with configurable search criteria• Detects continuation emails (JSON attachment) and restores state• Auto-loads envoy/states/{current_phase} note before each LLM call• Auto-loads context bundle notes when bundle_key is set• Calls OpenAI with structured output using fixed JSON schema• Phase-aware FSM: triage/gathering/summarising/working/coding/composing/waiting → complete/escalate• dispatch_immediate_actions(): write_notes, delete_notes, send_emails on every non-terminal phase• Sends continuation email when per-run iteration limit is hit or status='waiting'• execute_actions(): move_emails, delete_emails, confirmation reply on complete• Double-send prevention: tracks sent_to_addresses across dispatch_immediate_actions calls• Routes outbound email through dispatch table (sendmail or WebDAV)

dispatch_immediate_actions()

Runs on every iteration for every non-terminal phase. Signature:dispatch_immediate_actions(client, response, gather_results, sent_to_addresses=None)• Applies write_notes immediately (LLM can read them in the same run)• Applies delete_notes immediately• Sends send_emails and adds each recipient to sent_to_addresses• gather_results list is passed back to LLM in next iteration as feedback

webdav_deliver.py

WebDAV email delivery module:• WebDAVDelivery class connecting to webdav.critchley.biz/mail/john• Writes .eml files directly to maildir new/ folder• Credentials from ~/.netrc, uses webdav4.client• Unique filenames with 'E' prefix (avoids popit3 clashes)

imap_client.py

IMAP client interface:• Uses .netrc for credentials• Context manager for safe resource cleanup• list_folders(), select_folder(), search(), fetch()• move_message(), delete_message(), create_folder(), expunge()• append() — used for Sent copies of outbound emails and continuation emails• Auto-creates folders when moving emails to non-existent destinations

envoy_schema.py

Pydantic models defining the structured response contract:• EnvoyResponse — Main response schema• EmailOut — Outgoing email structure• EmailMove — Email move operations• EmailFetch — Fetch by Message-ID with folder hint• EmailSearch — IMAP search criteria with folder and flags• NoteWrite — Notes system write operations

test_orchestrator.py

Test harness for manual and automated testing. See Testing Guide.• inject_email() — APPEND to INBOX + mark UNSEEN• resume_latest_continuation() — find latest continuation in Sent, inject to INBOX• mark_unseen() — mark specific message UNSEEN by sequence number• list_inbox() — list INBOX messages with flags• run_orchestrator() — subprocess call with --max-iterations• CLI: --inject SUBJECT, --body, --resume, --mark-unseen, --list, --run, --max-iter N

IMAP Connection Architecture

Critical rule: The orchestrator opens ONE IMAP connection in main() and passes it to all functions. Never open additional connections — IMAP sequence numbers are session-specific and become stale across connections.

Functions that take the client parameter: fetch_unread_emails, fetch_emails_by_id, search_emails_imap, execute_actions.

Move and delete operations re-lookup emails by Message-ID header in the current session to avoid stale sequence numbers. Multi-folder search: INBOX first, then Active.

Multi-Mailbox Support

• Folder list fetched on startup and injected into LLM context• list_folders boolean field on EnvoyResponse to refresh folder list• EmailFetch model: per-email folder hint for faster cross-folder lookup• Fallback search order: hinted folder → INBOX → Done → other folders• LLM can search any folder via EmailSearch.folder field

Continuation Emails

See Continuation Emails for full details.

• Per-run limit (default 8, configurable via --max-iterations)• Total limit across continuations (24)• MIME email with JSON attachment carries: working_note, bundle_key, current_phase, note keys, email refs, failed_fetches• Human-readable text body with working note and references• Best-effort Sent IMAP copy stored immediately after sending• Automatic cleanup of continuation emails after processing

Email Delivery & Routing

See Email Delivery & Address Routing.

• EMAIL_DISPATCH table: maps addresses to (delivery_fn, target_address)• deliver_via_mail — sends via sendmail (default)• WebDAVDelivery.deliver — writes .eml to WebDAV maildir• resolve_delivery() returns (fn, addr) for routing• map_email_address() returns target addr for reply dedup

OpenAI Integration

OpenAI's strict: True mode requires fully expanded schemas (no $ref, additionalProperties: false on all objects). Schema is maintained manually in new_envoy_response_schema.json and loaded at runtime.

Model tiers: nano (gpt-4.1-nano), mini (gpt-4.1-mini), full (gpt-5.2-chat-latest). LLM hints next iteration's tier via next_model field. Default tier is set per phase in envoy/states/* notes.

Command Line Options

python3 orchestrator.py \
  --max-iterations 8 \
  --start-notes envoy/start \
  --folder INBOX \
  --search UNSEEN

• --max-iterations — Per-run iteration limit before continuation (default 8)• --start-notes — Comma-separated context note keys (default envoy/start)• --folder — IMAP folder to check (default INBOX)• --search — IMAP search criteria (default UNSEEN)

Configuration

• IMAP Host: imap (credentials in ~/.netrc)• Start Note: envoy/start• Model Tiers: gpt-4.1-nano / gpt-4.1-mini / gpt-5.2-chat-latest• API Key: From OPENAI_API_KEY environment variable• WebDAV: webdav.critchley.biz (credentials in ~/.netrc)

Repository

Code at /home/john/py/envoy/ (git repository)

version2
updated2026-02-22