Skip to main content
Component: Email request/response contract (issue #1262) Module: gaia_agent_email.contract Validation: pydantic v2 Schema version: 1.0

Overview

A frozen, stable request/response schema for the Email Triage Agent, shared by the REST surface (#1229) and the MCP stdio interface (#1104). GAIA owns this contract — the consuming application conforms to it, not the other way around. It is frozen here so the dependent endpoints can be built against a stable shape. Key properties:
  • One schema, two surfaces. REST and MCP stdio import the same pydantic models, guaranteeing identical structured output for a fixed input.
  • Single email and full thread. The input is a discriminated union on a kind field ("single" / "thread"); a consumer branches deterministically.
  • Dependency-light. gaia_agent_email.contract imports only pydantic — no Gmail or connector backends — so either surface can import it without pulling live-mail machinery into the process. (A regression test enforces this.)
  • Fail loudly. Every model forbids unknown fields (extra="forbid"). An off-contract payload raises a ValidationError naming the offending field, never a silently coerced result.
  • Versioned. SCHEMA_VERSION ("1.0") is pinned in the module and echoed in both request and response so a consumer can detect a breaking change.

Request schema (input)

EmailTriageRequest — top-level envelope.
FieldTypeNotes
schema_versionstringContract version. Defaults to "1.0".
payloadSingleEmailInput | ThreadInputDiscriminated on kind.

Shared input fields

Both payload shapes carry a principal — the recipient the agent acts on behalf of (the inbox owner). This is distinct from a message’s to: in a thread the principal is not necessarily a recipient of every message. EmailAddress:
FieldTypeNotes
namestring | nullDisplay name. Optional.
emailstringRequired. Rejected loudly if it lacks @ or a dotted domain.
EmailMessage:
FieldTypeNotes
message_idstringProvider message id. Required.
thread_idstring | nullOwning thread id.
fromEmailAddressSender. On the wire the key is from; in Python the field is from_ (keyword clash). Required.
toEmailAddress[]Primary recipients.
ccEmailAddress[]Carbon copies.
bccEmailAddress[]Blind carbon copies.
datestring | nullISO-8601 timestamp.
subjectstringSubject line.
bodystringPlain-text body to analyze. Required.

SingleEmailInput (kind: "single")

FieldTypeNotes
kind"single"Discriminator.
principalEmailAddressInbox owner. Required.
messageEmailMessageThe one message to analyze. Required.

ThreadInput (kind: "thread")

FieldTypeNotes
kind"thread"Discriminator.
principalEmailAddressInbox owner. Required.
thread_idstringConversation id. Required.
messagesEmailMessage[]Non-empty, oldest-first. An empty thread is rejected loudly.

Response schema (output)

EmailTriageResponse — top-level envelope.
FieldTypeNotes
schema_versionstringEchoes the contract version.
request_kind"single" | "thread"Which input shape produced the result.
resultEmailTriageResultThe structured analysis.
EmailTriageResult:
FieldTypeNotes
categoryenumOne of urgent, actionable, informational, low priority. Matches the agent’s frozen four-bucket taxonomy.
is_spamboolSpam signal, scored independently of category.
is_phishingboolPhishing signal, independent of is_spam.
summarystringPlain-text summary of the email / thread. Required.
action_itemsActionItem[]Extracted actions. May be empty.
draftDraftReply | nullProposed reply, or null when none is suggested. Never sent without explicit confirmation (#1264).
ActionItem:
FieldTypeNotes
descriptionstringImperative action. Required, non-empty.
due_hintstring | nullFree-text due hint as written ("Friday"); not parsed into a date.
DraftReply:
FieldTypeNotes
toEmailAddress[]Non-empty proposed recipients.
subjectstringProposed subject.
bodystringProposed body.

Example — single email

Request

{
  "schema_version": "1.0",
  "payload": {
    "kind": "single",
    "principal": { "name": "Alice Example", "email": "[email protected]" },
    "message": {
      "message_id": "msg-1",
      "thread_id": "thread-1",
      "from": { "name": "Bob Sender", "email": "[email protected]" },
      "to": [{ "name": "Alice Example", "email": "[email protected]" }],
      "cc": [],
      "date": "2026-05-30T09:00:00Z",
      "subject": "Q2 invoice attached",
      "body": "Hi Alice, please review the attached invoice by Friday."
    }
  }
}

Response

{
  "schema_version": "1.0",
  "request_kind": "single",
  "result": {
    "category": "actionable",
    "is_spam": false,
    "is_phishing": false,
    "summary": "Vendor invoice needs review by Friday.",
    "action_items": [
      { "description": "Review the Q2 invoice", "due_hint": "Friday" }
    ],
    "draft": {
      "to": [{ "name": "Bob Sender", "email": "[email protected]" }],
      "subject": "Re: Q2 invoice attached",
      "body": "Thanks Bob, I'll review and confirm by Friday."
    }
  }
}

Example — full thread

Request

{
  "schema_version": "1.0",
  "payload": {
    "kind": "thread",
    "principal": { "name": "Alice Example", "email": "[email protected]" },
    "thread_id": "thread-42",
    "messages": [
      {
        "message_id": "msg-1",
        "thread_id": "thread-42",
        "from": { "name": "Bob", "email": "[email protected]" },
        "to": [{ "name": "Alice", "email": "[email protected]" }],
        "date": "2026-05-30T09:00:00Z",
        "subject": "Contract renewal",
        "body": "Can we hop on a call about the renewal?"
      },
      {
        "message_id": "msg-2",
        "thread_id": "thread-42",
        "from": { "name": "Alice", "email": "[email protected]" },
        "to": [{ "name": "Bob", "email": "[email protected]" }],
        "date": "2026-05-30T10:00:00Z",
        "subject": "Re: Contract renewal",
        "body": "Sure, does Thursday 2pm work?"
      }
    ]
  }
}

Response

{
  "schema_version": "1.0",
  "request_kind": "thread",
  "result": {
    "category": "actionable",
    "is_spam": false,
    "is_phishing": false,
    "summary": "Bob wants a renewal call; Alice proposed Thursday 2pm.",
    "action_items": [{ "description": "Confirm Thursday 2pm call" }],
    "draft": null
  }
}

Usage

Validate a payload at a boundary (REST endpoint, MCP tool handler). Both helpers raise loudly on a contract violation — never return a partial object:
from gaia_agent_email.contract import parse_request, parse_response

request = parse_request(raw_request_dict)   # -> EmailTriageRequest
if request.payload.kind == "thread":
    for message in request.payload.messages:
        ...

response = parse_response(raw_response_dict)  # -> EmailTriageResponse

Stability contract

  • Frozen at 1.0. Additive, backward-compatible changes (new optional fields) keep the version. Any breaking change (renamed/removed field, new required field, taxonomy change) bumps SCHEMA_VERSION so consumers detect it.
  • Categories never drift. The four-bucket taxonomy is mirrored from the agent’s triage_heuristics.ALL_CATEGORIES; a unit test asserts byte-for-byte equality, so a taxonomy change in either place fails CI.
  • Unknown fields are errors, not warnings — there is no silent forward-compat drift in either direction.