コンテンツにスキップ

Canonical response envelope (v2)

Status: opt-in (default = legacy shape). Plan reference: docs/_internal/value_maximization_plan_no_llm_api.md §28.2.

The v2 envelope unifies the wire shape that customer agents (Cursor / Cline / Continue / Claude Desktop / Zapier / Make / RPA / SDK consumers) pattern-match on. Pre-v2, AutonoMath emitted ~4 distinct success shapes ({total,limit,offset,results}, nested 360 blocks, AutonoMath-specific status/explanation/meta, raw pydantic dumps). v2 collapses every successful response into one shape; every error into one shape.

When to use v2

Use v2 when:

  • You are an AI/agent client that pattern-matches on status, error.code, or meta.request_id.
  • You want deterministic follow-up calls via suggested_actions[].
  • You want citations (citations[]) as a first-class field rather than parsing each row's inline source_url.
  • You want a single error shape for every 4xx/5xx instead of code-branch mapping the legacy union of {detail}, {detail, error}, {detail, limit, resets_at}, etc.

Use legacy when:

  • You are an existing browser frontend whose JS still reads body.total / body.results.
  • You are a CSV/XLSX/ICS exporter — those formats are produced by ?format=csv|xlsx|... and are unaffected by the envelope flag.
  • You are a dashboard / SLA monitor that already pattern-matches on the legacy am/health/deep shape.

How to opt in

Either path suffices (route consults whichever is present first):

  1. Query parameter:
GET /v1/programs/search?q=IT導入&envelope=v2
  1. Accept header:
GET /v1/programs/search?q=IT導入
Accept: application/vnd.jpcite.v2+json

The response carries an X-Envelope-Version: v2 header so a client can confirm which shape it received without parsing the body. Caches respect Vary: Accept, X-Envelope-Version so legacy and v2 callers share the same path safely.

Wire shape — success

{
  "status": "rich | sparse | empty | partial | error",
  "query_echo": {
    "normalized_input": {"q": "IT導入"},
    "applied_filters": {"tier": ["S","A"], "prefecture": "東京都"},
    "unparsed_terms": []
  },
  "results": [ /* rows, same shape as legacy results[] */ ],
  "citations": [
    {
      "source_id": "src-...",
      "source_url": "https://www.meti.go.jp/...",
      "publisher": "経済産業省",
      "title": "...",
      "fetched_at": "2026-04-29T03:00:00Z",
      "checksum": "sha256:...",
      "license": "pdl_v1.0",
      "field_paths": ["/results/0/amount_max_yen"],
      "verification_status": "verified"
    }
  ],
  "warnings": [],
  "suggested_actions": [
    {"tool": "get_program", "args": {"unified_id": "UNI-..."}},
    {"endpoint": "/v1/programs/{unified_id}", "args": {"unified_id": "UNI-..."}}
  ],
  "meta": {
    "request_id": "01KQ3XQ77RR7J8XWZ8C0YR2JN2",
    "api_version": "v2",
    "latency_ms": 42,
    "billable_units": 1,
    "client_tag": "顧問先-001"
  }
}

Status semantics:

status results required extras
rich ≥ 5 rows
sparse 1–4 rows (optional) retry_with
empty 0 rows empty_reason (no_match / filters_too_narrow / source_unavailable / license_blocked); retry_with recommended
partial ≥ 0 rows warnings[] MUST be non-empty
error [] error envelope (see below)

Wire shape — error

{
  "status": "error",
  "results": [],
  "warnings": [],
  "citations": [],
  "query_echo": {...},
  "error": {
    "code": "RATE_LIMITED",
    "user_message": "レート制限を超過しました。Retry-After ヘッダの秒数だけ待ってから再試行してください。",
    "developer_message": "anonymous IP quota: 3/日 (reset 2026-05-01T00:00+09:00)",
    "retryable": true,
    "retry_after": 60,
    "documentation": "https://jpcite.com/docs/api-reference/response_envelope#rate_limited"
  },
  "meta": {
    "request_id": "01KQ3XQ77RR7J8XWZ8C0YR2JN2",
    "api_version": "v2",
    "latency_ms": 1,
    "billable_units": 0
  }
}

Closed enum on error.code:

code retryable typical HTTP when
RATE_LIMITED true 429 per-second throttle exhausted; honour Retry-After
UNAUTHORIZED false 401 missing/invalid X-API-Key
FORBIDDEN false 403 auth ok but action not permitted (license-gate is separate)
NOT_FOUND false 404 resource lookup miss; agent should NOT retry the same key
VALIDATION_ERROR false 400/422 bad input field; check developer_message for the field path
LICENSE_GATE_BLOCKED false 403 row(s) dropped by ?license= filter or proprietary policy
QUOTA_EXCEEDED false 429 quota cap reached (anon 3/日 or paid customer-set monthly cap)
INTEGRITY_ERROR true 500 DB integrity / cross-source mismatch detected mid-request; retry once
INTERNAL_ERROR true 500 unexpected exception; developer_message carries the trace pointer

MCP shape

MCP tools wrap the v2 envelope in a CallToolResult per the 2025-06-18 spec:

{
  "structuredContent": { /* StandardResponse JSON */ },
  "content": [
    {"type": "text", "text": "rich · 23 results"}
  ]
}

Errors set "isError": true at the result root and surface error.user_message in the text content block.

Migrated routes (worked examples)

The following routes accept the ?envelope=v2 opt-in today. Other routes return the legacy shape regardless of the flag — they will be migrated incrementally.

Route Default shape v2 shape
GET /v1/programs/search {total, limit, offset, results} StandardResponse[Program]
GET /v1/houjin/{bangou} nested 360 block single-result StandardResponse[dict] with citations[]
GET /v1/am/health/deep {status, version, checks, timestamp_utc} single-result StandardResponse[dict] (status maps ok→rich, degraded/unhealthy→partial)

Sample v2 response

GET /v1/programs/search?q=IT導入&tier=S&envelope=v2:

{
  "status": "rich",
  "query_echo": {
    "normalized_input": {"q": "IT導入"},
    "applied_filters": {"tier": ["S"], "fields": "default", "limit": 20, "offset": 0},
    "unparsed_terms": []
  },
  "results": [ /* 7 program rows, same shape as legacy results[] */ ],
  "citations": [],
  "warnings": [],
  "suggested_actions": [],
  "meta": {
    "request_id": "01KQ3XQ77RR7J8XWZ8C0YR2JN2",
    "api_version": "v2",
    "latency_ms": 42,
    "billable_units": 1
  }
}

(citations[] is empty here because the rows already carry source_url inline; routes with separate provenance — e.g. /v1/houjin/{bangou} — populate citations[] directly.)

Migration timeline

  • 2026-04-30 (this commit) — v2 ships as opt-in via ?envelope=v2 / Accept: application/vnd.jpcite.v2+json. Three routes migrated as worked examples.
  • 2026-05-06 (launch) — v2 documented in API reference, MCP tools, and SDK READMEs as the recommended shape for AI/agent clients.
  • 2026-05-06 → 2026-08-04 (90-day window) — both shapes supported in parallel. Legacy callers see no change. v2 adoption tracked via X-Envelope-Version access logs.
  • 2026-08-04 — default flips to v2. Legacy callers must explicitly opt out via ?envelope=v1 for an additional 30-day grace period. After 2026-09-03 the legacy shape is removed.

Stability

  • The v2 wire shape is frozen for the launch window. Adding new top-level keys is permitted; renaming or removing existing ones requires api_version bump.
  • error.code is a closed enum. Adding a new code is permitted; renaming requires api_version bump.
  • Per-row payload (inside results[]) follows each route's own contract, unchanged from v1.

See also

  • src/jpintel_mcp/api/_envelope.py — Pydantic models + helper constructors (StandardResponse.rich, .empty, .partial, .error; StandardError.rate_limited, .unauthorized, .not_found, .bad_request, .internal, …).
  • src/jpintel_mcp/mcp/_envelope.pywrap_for_mcp(...) adapter for MCP CallToolResult.
  • src/jpintel_mcp/api/middleware/envelope_adapter.py — opt-in detection middleware (parses query+header, stamps X-Envelope-Version response header).
  • tests/test_response_envelope.py — 8-test suite covering rich/empty/error/MCP/opt-in/legacy paths.
  • analysis_wave18/response_envelope_audit_2026-04-30.md — pre-work audit (8 routes × 3 error paths).