{
  "service": "observance",
  "version": "1",
  "description": "Agent-native memory API. Provides structured memory storage with types, cognitive roles, namespace scoping, relational linking, and machine-readable guidance (agent_contract) on every operational response.",
  "authentication": {
    "scheme": "Bearer",
    "header": "Authorization",
    "key_format": "obs_live_{64 hex chars}",
    "registration_endpoint": "/v1/keys/register",
    "notes": [
      "API keys are hashed with SHA-256; plaintext is never stored.",
      "Keys in query strings are rejected with 400.",
      "Each API key maps to exactly one account."
    ]
  },
  "memory_lifecycle": {
    "states": [
      "active",
      "archived",
      "deleted"
    ],
    "terminal_states": [
      "deleted"
    ],
    "transitions": [
      {
        "from": null,
        "to": "active",
        "trigger": "POST /v1/memories"
      },
      {
        "from": "active",
        "to": "archived",
        "trigger": "POST /v1/memories/:id/archive"
      },
      {
        "from": "archived",
        "to": "active",
        "trigger": "POST /v1/memories/:id/unarchive"
      },
      {
        "from": "active",
        "to": "deleted",
        "trigger": "DELETE /v1/memories/:id"
      },
      {
        "from": "archived",
        "to": "deleted",
        "trigger": "DELETE /v1/memories/:id"
      }
    ]
  },
  "memory_fields": {
    "required_at_creation": {
      "type": {
        "type": "string",
        "enum": [
          "episodic",
          "semantic",
          "procedural"
        ]
      },
      "event_at": {
        "type": "ISO 8601 timestamp",
        "description": "When the remembered event occurred"
      },
      "content": {
        "description": "At least one of content_text or content_json is required"
      }
    },
    "optional_at_creation": {
      "agent_id": {
        "type": "string",
        "format": "agt_{ULID}",
        "description": "Auto-provisioned if omitted"
      },
      "agent_name": {
        "type": "string",
        "max_length": 200
      },
      "agent_platform": {
        "type": "string",
        "max_length": 50
      },
      "namespace": {
        "type": "string",
        "default": "default",
        "pattern": "lowercase alphanumeric + hyphens, 2-100 chars"
      },
      "cognitive_role": {
        "type": "string",
        "default": "observation",
        "enum": [
          "observation",
          "reflection",
          "fact",
          "rule",
          "skill",
          "preference"
        ]
      },
      "source": {
        "type": "string",
        "default": "agent",
        "enum": [
          "agent",
          "system",
          "user",
          "derived"
        ]
      },
      "origin_type": {
        "type": "string",
        "default": "direct",
        "enum": [
          "direct",
          "summarized",
          "inferred",
          "imported"
        ]
      },
      "content_text": {
        "type": "string",
        "max_bytes": 32768
      },
      "content_json": {
        "type": "object (JSONB)",
        "max_bytes": 65536
      },
      "summary": {
        "type": "string",
        "max_length": 500
      },
      "importance_score": {
        "type": "number",
        "default": 0.5,
        "min": 0,
        "max": 1
      },
      "confidence_score": {
        "type": "number",
        "default": 1,
        "min": 0,
        "max": 1
      },
      "decay_policy": {
        "type": "string",
        "default": "exponential",
        "enum": [
          "none",
          "linear",
          "exponential"
        ]
      },
      "expires_at": {
        "type": "ISO 8601 timestamp or null",
        "default": null
      },
      "embedding_id": {
        "type": "string or null",
        "default": null
      },
      "metadata_json": {
        "type": "object (JSONB)",
        "max_bytes": 16384,
        "default": "{}"
      },
      "idempotencyKey": {
        "type": "string",
        "max_length": 255,
        "scope": "request-only, not stored on memory"
      }
    },
    "immutable_after_creation": [
      "type",
      "agent_id",
      "source",
      "origin_type",
      "event_at",
      "namespace"
    ],
    "system_managed": [
      "id",
      "status",
      "relevanceScore",
      "accessCount",
      "lastAccessedAt",
      "createdAt",
      "updatedAt"
    ]
  },
  "namespaces": {
    "description": "Organizational scoping for memories within an account.",
    "default": "default",
    "format": "Lowercase letters, digits, hyphens. 2-100 chars. Must start/end with letter or digit.",
    "cross_namespace_links": true,
    "notes": "Namespace is organizational, not a security boundary. Account isolation is the security boundary."
  },
  "agents": {
    "model": "multi-agent-per-account",
    "auto_provisioning": true,
    "id_format": "agt_{ULID}",
    "description": "An agent is a stable attribution profile for a role (e.g. sales-assistant, ingestion-worker, evaluator, admin), not a compute worker. Agents are not a concurrency guarantee — rate limits are tied to API keys, not agent IDs. For independent rate budgets across parallel agents, mint derived API keys via POST /v1/keys (admin scope).",
    "auto_provisioning_rule": "Omitting agent_id on POST /v1/memories uses the account default agent (auto-provisioned on first write). Only create new agent_ids for stable role separation — DO NOT create a fresh agent_id per request, per session, or per user message.",
    "cap": "Each plan caps the number of distinct agents per account. See billing.limits.agents.",
    "endpoints": [
      {
        "method": "GET",
        "path": "/v1/agents",
        "purpose": "List agents with memory counts"
      },
      {
        "method": "GET",
        "path": "/v1/agents/:id",
        "purpose": "Get single agent with memory count"
      },
      {
        "method": "PATCH",
        "path": "/v1/agents/:id",
        "purpose": "Update agent metadata"
      }
    ]
  },
  "links": {
    "relation_types": [
      "derived_from",
      "summarizes",
      "contradicts",
      "supports",
      "relates_to",
      "supersedes"
    ],
    "unique_constraint": "(account_id, from_memory_id, to_memory_id, relation_type)",
    "self_link_prevention": true,
    "cascade_on_delete": "Links are set to inactive when a connected memory is deleted.",
    "cascade_on_archive": "No effect. Links remain active when a memory is archived.",
    "duplicate_handling": "Duplicate creation returns the existing link (200). Inactive duplicates are reactivated."
  },
  "traversal": {
    "endpoint": "/v1/memories/:id/related",
    "depth": {
      "min": 1,
      "max": 3,
      "default": 1
    },
    "max_nodes": {
      "min": 1,
      "max": 200,
      "default": 50
    },
    "query_params": [
      "depth",
      "max_nodes",
      "direction",
      "relation_type",
      "order"
    ],
    "direction_filter": [
      "outgoing",
      "incoming",
      "both"
    ],
    "relation_type_filter": true,
    "order": [
      "depth",
      "score",
      "recency"
    ],
    "cycle_handling": "Per-branch visited[] prevents revisits within a single walk; the same memory can appear in different branches at different depths.",
    "memory_score": "0.5 × importance_score + 0.25 × confidence_score + 0.25 × relevance_score (traversal-only; not the same as search matchScore).",
    "response_shape": "V1 mode (no V1.5 query params): items[].link with single relation reference. V1.5 mode (any of depth/max_nodes/order/direction/relation_type set): items[] include depth, memory_score, and path[] with traversed_as direction.",
    "deleted_memory_filter": "Deleted memories are excluded from traversal results.",
    "archived_memory_behavior": "Archived memories are included in traversal results."
  },
  "search": {
    "keyword": {
      "endpoint": "GET /v1/memories?search=...",
      "style": "PostgreSQL websearch_to_tsquery + composite ranking (45% text match, 25% importance, 10% relevance, 10% confidence, 10% link connectivity).",
      "response_field": "matchScore (rounded to 3 decimals) on each item; query-relative — not globally comparable across searches.",
      "debug_mode": "Append ?debug=1 to a keyword search to receive scoreBreakdown per item: per-component weight, raw value, and contribution. Counts as a normal read; subject to standard read quota and rate limits.",
      "best_practice": "1–3 precise keywords or a short noun phrase; align write summaries with how callers will search."
    },
    "semantic_and_hybrid": {
      "endpoint": "POST /v1/memories/search",
      "modes": [
        "keyword",
        "semantic",
        "hybrid"
      ],
      "semantic": "Cosine similarity over client-supplied 1536-dimensional embeddings stored in memory_embeddings (pgvector ≥ 0.5, HNSW index).",
      "hybrid": "Reciprocal rank fusion (RRF) over keyword and semantic ranked lists. Default k = 60. Strict fail-fast: either side erroring fails the request.",
      "embedding_storage": "PUT /v1/memories/:id/embedding — client-supplied vectors only; the API does not generate embeddings.",
      "embedding_contract": {
        "dimensions": 1536,
        "metric": "cosine"
      },
      "mode_options": "Per-mode allow-listed (e.g. semantic accepts min_score, hybrid accepts k and min_score). Unknown keys return 400 mode_options_mismatch."
    }
  },
  "idempotency": {
    "supported_on": [
      "POST /v1/memories",
      "POST /v1/memories/batch (per-item)"
    ],
    "key_header": "Idempotency-Key (passed in request body as idempotencyKey)",
    "key_max_length": 255,
    "key_format": "Printable ASCII (0x20-0x7E)",
    "scope": "Per API key + key value",
    "ttl_days": 7,
    "stale_placeholder_recovery_seconds": 90,
    "behaviors": {
      "same_key_same_payload": "Returns cached 201 response",
      "same_key_different_payload": "409 idempotency_conflict",
      "in_flight": "503 idempotency_in_flight with retry_after_seconds: 2"
    }
  },
  "guide": {
    "endpoint": "/v1/guide",
    "customization_endpoint": "PATCH /v1/guide/hints",
    "description": "Structured operational guidance for agents. Returns Observance defaults when unauthenticated, or merged defaults + account overrides when authenticated.",
    "override_semantics": "REPLACE. PATCH /v1/guide/hints replaces the entire stored override state. The request body IS the complete desired overrides — it is not merged with previous overrides.",
    "overridable_sections": [
      "write_policy.store_when (full replacement — all 6 categories required)",
      "write_policy.avoid_storing (full array replacement)",
      "write_policy.importance (full object replacement with high/medium/low keys)"
    ],
    "non_overridable": "core_rules, reading_discipline, search_discipline, writing_discipline, linking, operations.",
    "reset": "Sending an empty body {} is rejected. To revert, send overrides matching defaults."
  },
  "rate_limits": {
    "note": "V1 uses in-memory stores (per-process). Not suitable for horizontal scaling without a shared store.",
    "limits": [
      {
        "endpoint": "POST /v1/keys/register",
        "limit": "10/hour",
        "key": "IP"
      },
      {
        "endpoint": "POST /v1/memories",
        "limit": "120/hour",
        "key": "API key"
      },
      {
        "endpoint": "POST /v1/memories/batch",
        "limit": "1 request/hour (quota counts per memory)",
        "key": "API key"
      },
      {
        "endpoint": "GET /v1/memories, GET /v1/memories/:id",
        "limit": "3000/hour",
        "key": "API key"
      },
      {
        "endpoint": "PATCH, DELETE, archive, unarchive",
        "limit": "600/hour",
        "key": "API key"
      },
      {
        "endpoint": "POST /v1/memories/:id/links",
        "limit": "600/hour",
        "key": "API key"
      },
      {
        "endpoint": "GET /v1/memories/:id/links, GET /v1/memories/:id/related",
        "limit": "3000/hour",
        "key": "API key"
      },
      {
        "endpoint": "DELETE /v1/links/:id",
        "limit": "600/hour",
        "key": "API key"
      },
      {
        "endpoint": "GET /v1/agents, GET /v1/agents/:id",
        "limit": "3000/hour",
        "key": "API key"
      },
      {
        "endpoint": "PATCH /v1/agents/:id",
        "limit": "600/hour",
        "key": "API key"
      },
      {
        "endpoint": "PATCH /v1/guide/hints",
        "limit": "600/hour",
        "key": "API key"
      },
      {
        "endpoint": "Discovery endpoints",
        "limit": "60/minute",
        "key": "IP"
      },
      {
        "endpoint": "All routes (global)",
        "limit": "6000/hour",
        "key": "IP"
      }
    ]
  },
  "endpoints": {
    "public": [
      {
        "method": "POST",
        "path": "/v1/keys/register",
        "purpose": "Register new API key"
      },
      {
        "method": "GET",
        "path": "/v1/capabilities",
        "purpose": "API capabilities document"
      },
      {
        "method": "GET",
        "path": "/v1/tool",
        "purpose": "MCP-compatible tool manifest"
      },
      {
        "method": "GET",
        "path": "/v1/schema",
        "purpose": "OpenAPI 3.1 schema document"
      },
      {
        "method": "GET",
        "path": "/.well-known/agent.json",
        "purpose": "Agent discovery manifest"
      },
      {
        "method": "GET",
        "path": "/v1/guide",
        "purpose": "Operational guide (pull-based, defaults or merged with account overrides)"
      },
      {
        "method": "GET",
        "path": "/v1/skill",
        "purpose": "Onboarding and usage guidance (structured JSON)"
      },
      {
        "method": "GET",
        "path": "/health",
        "purpose": "Health check"
      }
    ],
    "authenticated": [
      {
        "method": "POST",
        "path": "/v1/memories",
        "purpose": "Create memory"
      },
      {
        "method": "POST",
        "path": "/v1/memories/batch",
        "purpose": "Create multiple memories (partial success, per-item idempotency)"
      },
      {
        "method": "GET",
        "path": "/v1/memories",
        "purpose": "List memories (filtered, paginated, keyword search via ?search=, debug breakdown via ?debug=1)"
      },
      {
        "method": "GET",
        "path": "/v1/memories/:id",
        "purpose": "Get single memory"
      },
      {
        "method": "PATCH",
        "path": "/v1/memories/:id",
        "purpose": "Update mutable memory fields"
      },
      {
        "method": "DELETE",
        "path": "/v1/memories/:id",
        "purpose": "Soft-delete memory"
      },
      {
        "method": "POST",
        "path": "/v1/memories/:id/archive",
        "purpose": "Archive memory"
      },
      {
        "method": "POST",
        "path": "/v1/memories/:id/unarchive",
        "purpose": "Unarchive memory"
      },
      {
        "method": "POST",
        "path": "/v1/memories/:id/links",
        "purpose": "Create memory link"
      },
      {
        "method": "GET",
        "path": "/v1/memories/:id/links",
        "purpose": "List links for a memory"
      },
      {
        "method": "DELETE",
        "path": "/v1/links/:id",
        "purpose": "Soft-delete link"
      },
      {
        "method": "GET",
        "path": "/v1/memories/:id/related",
        "purpose": "Traversal — depth 1–3, max_nodes 1–200, direction/relation_type/order params"
      },
      {
        "method": "POST",
        "path": "/v1/memories/search",
        "purpose": "Search memories — keyword / semantic / hybrid modes (RRF fusion)"
      },
      {
        "method": "PUT",
        "path": "/v1/memories/:id/embedding",
        "purpose": "Upload client-supplied 1536-dim embedding (cosine, normalized)"
      },
      {
        "method": "GET",
        "path": "/v1/keys",
        "purpose": "List API keys for this account (admin scope)"
      },
      {
        "method": "POST",
        "path": "/v1/keys",
        "purpose": "Mint a derived API key with read/write/admin scope (admin scope)"
      },
      {
        "method": "PATCH",
        "path": "/v1/keys/:id",
        "purpose": "Update key label (admin scope)"
      },
      {
        "method": "DELETE",
        "path": "/v1/keys/:id",
        "purpose": "Revoke an API key (admin scope; last admin key is protected)"
      },
      {
        "method": "GET",
        "path": "/v1/agents",
        "purpose": "List agents"
      },
      {
        "method": "GET",
        "path": "/v1/agents/:id",
        "purpose": "Get single agent"
      },
      {
        "method": "PATCH",
        "path": "/v1/agents/:id",
        "purpose": "Update agent metadata"
      },
      {
        "method": "PATCH",
        "path": "/v1/guide/hints",
        "purpose": "Customize guide hints for this account"
      },
      {
        "method": "GET",
        "path": "/v1/billing/usage",
        "purpose": "Current usage and limits (does not count as a read)"
      },
      {
        "method": "GET",
        "path": "/v1/billing/status",
        "purpose": "Plan state and purchase history (does not count as a read)"
      },
      {
        "method": "POST",
        "path": "/v1/billing/checkout",
        "purpose": "Start plan upgrade checkout"
      },
      {
        "method": "POST",
        "path": "/v1/billing/confirm",
        "purpose": "Confirm payment and activate plan"
      }
    ]
  },
  "error_model": {
    "shape": "{ error, message, request_id, agent_contract }",
    "codes": [
      {
        "code": "missing_api_key",
        "http": 401,
        "retryable": true
      },
      {
        "code": "invalid_api_key",
        "http": 401,
        "retryable": false
      },
      {
        "code": "invalid_request",
        "http": 400,
        "retryable": false
      },
      {
        "code": "memory_not_found",
        "http": 404,
        "retryable": false
      },
      {
        "code": "agent_not_found",
        "http": 404,
        "retryable": false
      },
      {
        "code": "link_not_found",
        "http": 404,
        "retryable": false
      },
      {
        "code": "content_required",
        "http": 400,
        "retryable": false
      },
      {
        "code": "immutable_field",
        "http": 400,
        "retryable": false
      },
      {
        "code": "invalid_transition",
        "http": 409,
        "retryable": false
      },
      {
        "code": "self_link",
        "http": 400,
        "retryable": false
      },
      {
        "code": "cross_account_link",
        "http": 400,
        "retryable": false
      },
      {
        "code": "idempotency_conflict",
        "http": 409,
        "retryable": false
      },
      {
        "code": "idempotency_in_flight",
        "http": 503,
        "retryable": true
      },
      {
        "code": "quota_exhausted",
        "http": 403,
        "retryable": false
      },
      {
        "code": "active_memory_limit",
        "http": 403,
        "retryable": false
      },
      {
        "code": "agent_limit_reached",
        "http": 403,
        "retryable": false
      },
      {
        "code": "read_quota_exceeded",
        "http": 429,
        "retryable": false
      },
      {
        "code": "invalid_plan",
        "http": 400,
        "retryable": false
      },
      {
        "code": "already_on_plan",
        "http": 409,
        "retryable": false
      },
      {
        "code": "checkout_failed",
        "http": 502,
        "retryable": true
      },
      {
        "code": "purchase_not_found",
        "http": 404,
        "retryable": false
      },
      {
        "code": "purchase_expired",
        "http": 410,
        "retryable": false
      },
      {
        "code": "capture_failed",
        "http": 502,
        "retryable": true
      },
      {
        "code": "rate_limited",
        "http": 429,
        "retryable": true
      },
      {
        "code": "server_error",
        "http": 500,
        "retryable": true
      }
    ]
  },
  "agent_contract": {
    "description": "Every operational response (success and error) includes an agent_contract field with machine-readable guidance. Agents should read next_actions to determine what to do, rather than parsing HTTP status codes.",
    "version": "1",
    "structure": {
      "version": "Always \"1\"",
      "retryable": "boolean — whether the same request can be retried",
      "next_actions": "Array of Action objects, each with action, available, recommended"
    },
    "action_codes": [
      "create_memory",
      "get_memory",
      "list_memories",
      "update_memory",
      "delete_memory",
      "archive_memory",
      "unarchive_memory",
      "create_link",
      "list_links",
      "delete_link",
      "find_related",
      "check_memory_status",
      "list_agents",
      "get_agent",
      "update_agent",
      "reuse_agent_id",
      "check_usage",
      "start_checkout",
      "confirm_payment",
      "upgrade_plan",
      "retry_next_cycle",
      "retry_after_wait",
      "authenticate",
      "fix_request"
    ],
    "stability_guarantee": {
      "breaking": "Removing or renaming action codes, error codes, or agent_contract fields requires /v2/",
      "non_breaking": "Adding new optional fields, action codes, or error codes stays in /v1/"
    }
  },
  "suite": {
    "name": "agent-infrastructure-suite",
    "role": "remember",
    "model": "remember → decide → produce",
    "siblings": {
      "orchestrion": {
        "role": "decide",
        "description": "Task orchestration and work scheduling"
      },
      "outputlayer": {
        "role": "produce",
        "description": "Artifact storage and delivery"
      }
    },
    "external_refs": {
      "description": "Memories can reference external resources via metadata_json.external_refs",
      "format": {
        "service": "string",
        "resource_type": "string",
        "resource_id": "string"
      }
    }
  },
  "limitations": [
    "Rate limiting uses in-memory stores — per-process only, not distributed.",
    "Traversal depth is bounded to 1–3 hops; max_nodes is bounded to 1–200 per walk.",
    "Embedding storage and similarity search require client-supplied 1536-dim vectors (PUT /v1/memories/:id/embedding). The API does not generate embeddings.",
    "Hybrid search uses Reciprocal Rank Fusion (RRF) and is strict fail-fast: if either keyword or semantic side errors, the request fails.",
    "No webhook or push notifications — consumers must poll.",
    "Batch creation available via POST /v1/memories/batch (max 50). No bulk update/delete."
  ]
}