Skip to content

Latest commit

 

History

History
1775 lines (1388 loc) · 44.4 KB

api_reference.md

File metadata and controls

1775 lines (1388 loc) · 44.4 KB

REST API Reference

Forgetful exposes a REST API for web UI integration and external access.

Base URL: http://localhost:8020/api/v1

Health and capability discovery use the application base URL, outside /api/v1.

Authentication

The REST API uses the same authentication provider as MCP routes. This means:

  • If you've configured OAuth2/OIDC for MCP, the same tokens work for REST
  • If auth is disabled (FASTMCP_SERVER_AUTH not set), REST endpoints use a default user
  • All FastMCP auth providers are supported (JWT, OAuth2, GitHub, Google, Azure, etc.)

When Auth is Enabled

Include a Bearer token in the Authorization header:

Authorization: Bearer <your-oauth-token>

The token is validated using mcp.auth.verify_token() - the same mechanism used for MCP tool authentication. User identity is extracted from the token's sub claim.

When Auth is Disabled (Default)

If FASTMCP_SERVER_AUTH is not set, domain requests use a default user. No Authorization header is required. Public capability discovery does not resolve or create a user.

Configuration

Authentication is configured via FastMCP environment variables:

# Example: GitHub OAuth
FASTMCP_SERVER_AUTH=github
GITHUB_CLIENT_ID=your_client_id
GITHUB_CLIENT_SECRET=your_client_secret

# Example: Bearer token (simple API key)
FASTMCP_SERVER_AUTH=bearer
FASTMCP_SERVER_AUTH_SECRET=your-secret-key

See FastMCP Auth Documentation for all supported providers and configuration options.

Common Response Codes

Code Description
200 Success
201 Created
400 Bad Request (validation error, CyclicDependencyError)
401 Unauthorized (missing/invalid token)
404 Not Found (also returned when a referenced project_id does not exist or belongs to another user)
409 Conflict (ConflictError -- version mismatch in optimistic concurrency)
422 Unprocessable Entity (InvalidStateTransitionError, DependencyNotMetError)
500 Internal Server Error

Health

GET /health

Health check endpoint (no authentication required).

Response:

{
  "status": "healthy",
  "timestamp": "2024-12-05T10:00:00Z",
  "service": "forgetful",
  "version": "0.3.0"
}

Capability discovery

GET /.well-known

Public JSON metadata describing which optional features the instance has enabled. No credentials are required, even when authentication is enabled. All callers receive the same schema version and capability booleans; the response does not expose raw configuration or user data.

The path is relative to the application's deployment base, not /api/v1 or the MCP endpoint:

  • Root deployment: https://host/.well-known.
  • Deployment at https://host/forgetful/: https://host/forgetful/.well-known.

Preserve the deployment prefix when building this URL. /forgetful/ is an example prefix, not a hardcoded route. This project-specific endpoint is separate from OAuth's standard well-known routes.

Response (example):

{
  "schema_version": 1,
  "capabilities": {
    "planning": true,
    "skills": false,
    "files": true,
    "activity_stream": false
  }
}

Each value maps directly to the settings singleton:

Capability Setting Meaning
planning PLANNING_ENABLED Plans and tasks are enabled together.
skills SKILLS_ENABLED Skill operations are enabled.
files FILES_ENABLED File operations are enabled.
activity_stream ACTIVITY_ENABLED Activity tracking and live streaming are enabled.

activity_stream: false does not disable queries of previously stored activity history.

Discovery remains available when all optional features are disabled. Each request reads settings without a server-side response cache, database queries, model calls, or user creation. Clients own caching and refresh policy. These booleans describe configuration, not service health, the caller's permissions, or a guarantee that a feature request will succeed. Feature routes keep their normal authentication and authorization requirements.

For schema version 1, clients should ignore additional capability keys. Only an explicit false means disabled; a missing key, unsupported schema version, or unavailable document means unknown. Older servers may not provide this endpoint. Clients may fall back to probing a feature route, but must not treat authentication errors, routing errors, or temporary outages as a disabled feature.


Memories

GET /api/v1/memories

List memories with pagination, sorting, and filtering.

Query Parameters:

Parameter Type Default Description
limit int 20 Results per page (1-100)
offset int 0 Skip N results
sort_by string created_at Sort field: created_at, updated_at, importance
sort_order string desc Sort direction: asc, desc
project_id int - Filter by project
importance_min int - Minimum importance (1-10)
tags string - Comma-separated tags
include_obsolete bool false Include obsolete memories

Response:

{
  "memories": [
    {
      "id": 1,
      "title": "Memory title",
      "content": "Memory content...",
      "importance": 7,
      "tags": ["tag1", "tag2"],
      "created_at": "2024-12-05T10:00:00Z",
      "updated_at": "2024-12-05T10:00:00Z",
      "linked_memory_ids": [2, 3],
      "project_ids": [1],
      "source_repo": "owner/repo",
      "source_files": ["path/to/file.py"],
      "source_url": "https://example.com/source",
      "confidence": 0.85,
      "encoding_agent": "claude-sonnet-4",
      "encoding_version": "1.0.0",
      "access_count": 0,
      "last_accessed_at": null
    }
  ],
  "total": 42,
  "limit": 20,
  "offset": 0
}

GET /api/v1/memories/{id}

Get a single memory by ID.

Response: Memory object (see above)

The object includes server-managed access_count (int, default 0) and last_accessed_at (ISO datetime or null). Callers cannot set them on POST/PUT; incrementing them does not change updated_at. On upstream, counters increment only when memory.read / memory.queried events fire (get_memory / query_memory), which requires ACTIVITY_TRACK_READS=true and a wired EventBus (default off → counters stay 0). REST listing does not increment them.

POST /api/v1/memories

Create a new memory.

A configured Memory Plugin can allow, warn, or reject before persistence, and advise after a successful save. Successful create, update, and DELETE responses add plugin_warnings only when nonempty, for example:

{"plugin_warnings": [{"outcome": "warn", "message": "Check the source.", "code": "source"}]}

This field augments the normal response; existing lists (including obsolete_matches) remain. Warnings are not stored or returned by reads, lists, queries, or events. In enforce mode, rejection returns 400 with {"error": "<plugin message>"}; before-handler failure/timeout returns 503 with {"error": "Memory Write Plugin unavailable."}. In warn mode both proceed with warnings. After-handler failures preserve success and add a generic warning with code MEMORY_PLUGIN_AFTER_SAVE_UNAVAILABLE. Near-duplicate warnings use create's existing similarity results after persistence; they do not prevent duplicates. External cancellation or network failure can still lose a response after a write; verify state before retrying.

Request Body:

{
  "title": "Memory title",
  "content": "Memory content...",
  "context": "Why this is being stored",
  "keywords": ["keyword1", "keyword2"],
  "tags": ["tag1"],
  "importance": 7,
  "project_ids": [1],
  "source_repo": "owner/repo",
  "source_files": ["path/to/file.py"],
  "source_url": "https://example.com/source",
  "confidence": 0.85,
  "encoding_agent": "claude-sonnet-4",
  "encoding_version": "1.0.0"
}

Provenance Fields (all optional):

Field Type Description
source_repo string Repository source (e.g., 'owner/repo', max 200 chars)
source_files string[] List of file paths that informed this memory
source_url string URL to original source material (max 2048 chars)
confidence float Encoding confidence score (0.0-1.0)
encoding_agent string Agent/process that created this memory (max 100 chars)
encoding_version string Version of encoding process/prompt (max 50 chars)

Response (201):

{
  "id": 1,
  "title": "Memory title",
  "linked_memory_ids": [],
  "project_ids": [1],
  "code_artifact_ids": [],
  "document_ids": [],
  "similar_memories": []
}

PUT /api/v1/memories/{id}

Update an existing memory.

Request Body: (all fields optional)

{
  "title": "Updated title",
  "content": "Updated content",
  "importance": 8,
  "tags": ["updated-tag"],
  "source_repo": "owner/repo",
  "source_files": ["path/to/file.py"],
  "source_url": "https://example.com/source",
  "confidence": 0.9,
  "encoding_agent": "manual-review",
  "encoding_version": "1.0.0"
}

Provenance fields can be added or updated after memory creation. See POST /api/v1/memories for field descriptions.

Response: Flat updated memory object, with plugin_warnings only when nonempty (see create above). No-op updates skip both phases. Before rejection returns 400; unavailable returns 503. An invalid merged candidate returns 400 through validation handling.

DELETE /api/v1/memories/{id}

Mark a memory as obsolete (soft delete). Registered obsolete handlers run in both phases. Before rejection/unavailability uses the same 400/503 responses as create. Successful responses include plugin_warnings only when nonempty; after-handler failures remain successful.

When supplied, reason must be a string and superseded_by must be an integer or null. Omitted fields default to "Marked obsolete via API" and null; omitting the body uses both defaults. Malformed JSON, a non-object body, or invalid field types return 400 before changing the memory, whether or not a plugin is configured.

Request Body: (optional)

{
  "reason": "No longer relevant",
  "superseded_by": 42
}

Response:

{
  "success": true
}

POST /api/v1/memories/search

Semantic search across memories.

Request Body:

{
  "query": "search query",
  "query_context": "why searching",
  "k": 10,
  "include_links": true,
  "importance_threshold": 5,
  "project_ids": [1, 2]
}

Response:

{
  "memories": [...],
  "total": 10
}

POST /api/v1/memories/{id}/links

Link memories together (bidirectional).

Request Body:

{
  "related_ids": [2, 3, 4]
}

Response:

{
  "linked_ids": [2, 3, 4]
}

GET /api/v1/memories/{id}/links

Get memories linked to this memory.

Query Parameters:

  • limit (int, default 20): Max linked memories to return

Response:

{
  "memory_id": 1,
  "linked_memories": [...]
}

DELETE /api/v1/memories/{id}/links/{target_id}

Remove a link between two memories (bidirectional).

Response:

{
  "success": true
}

Projects

GET /api/v1/projects

List all projects.

Query Parameters:

Parameter Type Description
status string Filter by status: active, archived, completed
repo_name string Match equivalent repository addresses or literal identifiers

Repository lookup recognises equivalent HTTPS, SSH, and host-qualified addresses. For example, github.com/ScottRBK/forgetful, https://github.com/ScottRBK/forgetful.git, and git@github.com:ScottRBK/forgetful.git match each other. A trailing slash is optional. Hostnames are case-insensitive; repository paths and explicit ports remain significant. Hostless paths such as group/subgroup/repo do not imply GitHub or another host. Unsupported or ambiguous forms use literal matching. Lookup returns every matching project and can be combined with the status filter; it does not perform partial-name or fuzzy search.

Response: ProjectSummary items (lightweight; no description or notes).

{
  "projects": [
    {
      "id": 1,
      "name": "Project Name",
      "project_type": "development",
      "status": "active",
      "repo_name": "owner/repo",
      "last_encoding_point": "5d41402abc4b2a76b9719d911017c592",
      "memory_count": 12,
      "created_at": "2024-12-05T10:00:00Z",
      "updated_at": "2024-12-05T10:00:00Z"
    }
  ],
  "total": 5
}

GET /api/v1/projects/{id}

Get a single project by ID.

Response: Full Project object (includes description, notes, provenance fields, memory_count, and last_encoding_point when set). Shape differs from list ProjectSummary items.

POST /api/v1/projects

Create a new project.

repo_name is an optional Git repository identifier or address (maximum 255 characters). Nested paths, host-qualified identifiers, HTTPS/SSH addresses, and opaque values are accepted. Surrounding whitespace is trimmed; the supplied format is otherwise preserved in responses. An empty string clears the repository when updating a project. Invalid project fields return HTTP 400 with JSON field locations and error messages.

Request Body:

{
  "name": "Project Name",
  "description": "Project description",
  "project_type": "development",
  "repo_name": "owner/repo",
  "last_encoding_point": "5d41402abc4b2a76b9719d911017c592"
}

Project Types: personal, work, learning, development, infrastructure, template, product, documentation, open-source

last_encoding_point: Opaque, caller-managed checkpoint through which memories were last encoded for this project (e.g. a Git commit SHA). The server never infers or advances it; the encoding client updates it only after a successful run.

PUT /api/v1/projects/{id}

Update an existing project.

Request Body: (all fields optional)

{
  "name": "Updated Name",
  "description": "Updated description",
  "status": "completed",
  "last_encoding_point": "7d793037a0760186574b0282f2ebc2eb"
}

Omit last_encoding_point to leave the stored value unchanged; send an empty string to clear it.

DELETE /api/v1/projects/{id}

Delete a project (preserves associated memories).


Entities

GET /api/v1/entities

List all entities.

Query Parameters:

Parameter Type Description
entity_type string Filter by type: Individual, Organization, Team, Device, System, Other

Response:

{
  "entities": [
    {
      "id": 1,
      "name": "Entity Name",
      "entity_type": "Individual",
      "custom_type": null,
      "notes": "Some notes",
      "created_at": "2024-12-05T10:00:00Z"
    }
  ],
  "total": 10
}

GET /api/v1/entities/{id}

Get a single entity by ID.

POST /api/v1/entities

Create a new entity.

Request Body:

{
  "name": "Entity Name",
  "entity_type": "Individual",
  "custom_type": "Custom Type (if entity_type is Other)",
  "notes": "Optional notes"
}

PUT /api/v1/entities/{id}

Update an existing entity.

DELETE /api/v1/entities/{id}

Delete an entity.

POST /api/v1/entities/search

Search entities by name.

Request Body:

{
  "query": "search term",
  "entity_type": "Individual",
  "limit": 10
}

POST /api/v1/entities/{id}/memories

Link an entity to a memory.

Request Body:

{
  "memory_id": 1
}

DELETE /api/v1/entities/{id}/memories/{memory_id}

Remove link between entity and memory.

GET /api/v1/entities/{id}/relationships

Get relationships for an entity.

Response:

{
  "relationships": [
    {
      "id": 1,
      "source_entity_id": 1,
      "target_entity_id": 2,
      "relationship_type": "manages",
      "description": "Team lead"
    }
  ],
  "total": 3
}

POST /api/v1/entities/{id}/relationships

Create a relationship between entities.

Request Body:

{
  "target_entity_id": 2,
  "relationship_type": "manages",
  "description": "Optional description"
}

PUT /api/v1/entities/relationships/{id}

Update an entity relationship.

DELETE /api/v1/entities/relationships/{id}

Delete an entity relationship.


Documents

GET /api/v1/documents

List all documents.

Query Parameters:

Parameter Type Description
project_id int Filter by project
document_type string Filter by type
tags string Comma-separated tags

Response:

{
  "documents": [
    {
      "id": 1,
      "title": "Document Title",
      "description": "Brief description",
      "content": "Full document content...",
      "document_type": "analysis",
      "tags": ["tag1"],
      "created_at": "2024-12-05T10:00:00Z"
    }
  ],
  "total": 5
}

Document Types: analysis, guide, specification, report, note, reference

GET /api/v1/documents/{id}

Get a single document by ID.

POST /api/v1/documents

Create a new document.

Request Body:

{
  "title": "Document Title",
  "description": "Brief description",
  "content": "Full document content...",
  "document_type": "analysis",
  "tags": ["tag1"]
}

PUT /api/v1/documents/{id}

Update an existing document.

DELETE /api/v1/documents/{id}

Delete a document.


Code Artifacts

GET /api/v1/code-artifacts

List all code artifacts.

Query Parameters:

Parameter Type Description
project_id int Filter by project
language string Filter by programming language
tags string Comma-separated tags

Response:

{
  "code_artifacts": [
    {
      "id": 1,
      "title": "Artifact Title",
      "description": "What this code does",
      "code": "def hello():\n    return 'Hello'",
      "language": "python",
      "tags": ["utility"],
      "created_at": "2024-12-05T10:00:00Z"
    }
  ],
  "total": 10
}

GET /api/v1/code-artifacts/{id}

Get a single code artifact by ID.

POST /api/v1/code-artifacts

Create a new code artifact.

Request Body:

{
  "title": "Artifact Title",
  "description": "What this code does",
  "code": "def hello():\n    return 'Hello'",
  "language": "python",
  "tags": ["utility"]
}

PUT /api/v1/code-artifacts/{id}

Update an existing code artifact.

DELETE /api/v1/code-artifacts/{id}

Delete a code artifact.


Skills

Requires: SKILLS_ENABLED=true

GET /api/v1/skills

List all skills.

Query Parameters:

Parameter Type Description
project_id int Filter by project
tags string Comma-separated tags (OR logic)
importance_threshold int Minimum importance level

Response:

{
  "skills": [
    {
      "id": 1,
      "name": "code-review",
      "description": "Systematic code review process",
      "license": "MIT",
      "tags": ["development", "review"],
      "importance": 8,
      "project_id": null,
      "created_at": "2024-12-05T10:00:00Z",
      "updated_at": "2024-12-05T10:00:00Z"
    }
  ],
  "total": 5
}

GET /api/v1/skills/search

Semantic search for skills by description similarity.

Query Parameters:

Parameter Type Default Description
query string (required) Search query
k int 5 Number of results
project_id int - Filter by project

GET /api/v1/skills/{id}

Get a single skill by ID (includes full content).

POST /api/v1/skills

Create a new skill.

Request Body:

{
  "name": "code-review",
  "description": "Systematic code review process",
  "content": "# Code Review\n\n## Steps\n1. Check for...",
  "license": "MIT",
  "compatibility": "Requires Python 3.12+",
  "allowed_tools": ["Read", "Grep"],
  "metadata": {"author": "team"},
  "tags": ["development", "review"],
  "importance": 8,
  "project_id": 1
}

PUT /api/v1/skills/{id}

Update an existing skill (PATCH semantics).

DELETE /api/v1/skills/{id}

Delete a skill.

POST /api/v1/skills/import

Import a skill from Agent Skills markdown format.

Request Body:

{
  "skill_md": "---\nname: code-review\ndescription: ...\n---\n\n# Content...",
  "project_id": 1,
  "importance": 8
}

GET /api/v1/skills/{id}/export

Export a skill to Agent Skills markdown format (SKILL.md).

Response:

{
  "skill_md": "---\nname: code-review\ndescription: ...\n---\n\n# Content..."
}

Graph

GET /api/v1/graph

Get full knowledge graph for visualization. Returns all nodes and edges for the user.

Query Parameters:

Parameter Type Default Description
node_types string memory,entity,project,document,code_artifact Comma-separated list of node types to include
project_id int - Filter to specific project
include_entities bool true Include entity nodes (deprecated, use node_types)
limit int 100 Max memories to include (max 500)
offset int 0 Number of memories to skip for pagination
sort_by string created_at Sort field: created_at, updated_at, or importance
sort_order string desc Sort direction: asc or desc

Node Types:

  • memory - Knowledge memories
  • entity - People, organizations, devices
  • project - Project contexts
  • document - Long-form documents
  • code_artifact - Code snippets

Edge Types:

  • memory_link - Memory-to-memory connections
  • entity_memory - Entity linked to memory
  • entity_relationship - Entity-to-entity relationship
  • memory_project - Memory linked to project
  • document_project - Document belongs to project
  • code_artifact_project - Code artifact belongs to project
  • memory_document - Memory linked to document
  • memory_code_artifact - Memory linked to code artifact

Response:

{
  "nodes": [
    {
      "id": "memory_1",
      "type": "memory",
      "label": "Memory Title",
      "data": {
        "id": 1,
        "title": "Memory Title",
        "importance": 7,
        "tags": ["tag1"],
        "created_at": "2024-12-05T10:00:00Z"
      }
    },
    {
      "id": "entity_1",
      "type": "entity",
      "label": "Entity Name",
      "data": {
        "id": 1,
        "name": "Entity Name",
        "entity_type": "Individual"
      }
    },
    {
      "id": "project_1",
      "type": "project",
      "label": "Project Name",
      "data": {
        "id": 1,
        "name": "Project Name",
        "project_type": "development",
        "status": "active"
      }
    }
  ],
  "edges": [
    {
      "id": "memory_1_memory_2",
      "source": "memory_1",
      "target": "memory_2",
      "type": "memory_link"
    },
    {
      "id": "memory_1_project_1",
      "source": "memory_1",
      "target": "project_1",
      "type": "memory_project"
    }
  ],
  "meta": {
    "memory_count": 50,
    "total_memory_count": 1243,
    "offset": 0,
    "limit": 100,
    "has_more": true,
    "entity_count": 10,
    "project_count": 3,
    "document_count": 5,
    "code_artifact_count": 8,
    "edge_count": 75,
    "memory_link_count": 25,
    "entity_relationship_count": 5,
    "entity_memory_count": 15,
    "memory_project_count": 12,
    "document_project_count": 5,
    "code_artifact_project_count": 8,
    "memory_document_count": 3,
    "memory_code_artifact_count": 2
  }
}

Pagination Examples:

# Get first 50 memories sorted by importance
GET /api/v1/graph?limit=50&sort_by=importance&sort_order=desc

# Get next page of memories
GET /api/v1/graph?limit=50&offset=50&sort_by=importance&sort_order=desc

# Filter to project and sort by creation date (oldest first)
GET /api/v1/graph?project_id=4&sort_by=created_at&sort_order=asc

GET /api/v1/graph/subgraph

Get subgraph centered on a specific node using efficient CTE traversal. This is the recommended endpoint for graph visualization with depth-limited traversal.

Query Parameters:

Parameter Type Default Description
node_id string required Center node (e.g., memory_1, entity_5, project_3, document_2, code_artifact_4)
depth int 2 Traversal depth (1-3, clamped)
node_types string memory,entity,project,document,code_artifact Comma-separated list of types to traverse
max_nodes int 200 Maximum nodes to return (max 500)

Response:

{
  "nodes": [
    {
      "id": "memory_1",
      "type": "memory",
      "depth": 0,
      "label": "Center Memory",
      "data": {...}
    },
    {
      "id": "memory_2",
      "type": "memory",
      "depth": 1,
      "label": "Linked Memory",
      "data": {...}
    }
  ],
  "edges": [...],
  "meta": {
    "center_node_id": "memory_1",
    "depth": 2,
    "node_types": ["memory", "entity"],
    "max_nodes": 200,
    "memory_count": 5,
    "entity_count": 2,
    "project_count": 0,
    "document_count": 0,
    "code_artifact_count": 0,
    "edge_count": 6,
    "memory_link_count": 3,
    "entity_relationship_count": 1,
    "entity_memory_count": 2,
    "memory_project_count": 0,
    "document_project_count": 0,
    "code_artifact_project_count": 0,
    "memory_document_count": 0,
    "memory_code_artifact_count": 0,
    "truncated": false
  }
}

GET /api/v1/graph/memory/{id}

Deprecated: Use /api/v1/graph/subgraph?node_id=memory_{id} instead.

Get subgraph centered on a specific memory.

Query Parameters:

Parameter Type Default Description
depth int 1 Link traversal depth (1-3)

Response:

{
  "nodes": [...],
  "edges": [...],
  "center_memory_id": 1,
  "meta": {
    "memory_count": 5,
    "edge_count": 4,
    "depth": 1
  }
}

Activity

Experimental Feature

Activity logging is an experimental feature. The async event-driven architecture may cause issues with SQLite backends due to connection pooling conflicts. If you are using SQLite and do not intend to use activity tracking, leave it disabled by not configuring the activity-related settings. PostgreSQL users can enable this feature but please be advised it is still experimental

Known limitations:

  • SQLite in-memory mode (testing): Events may conflict with concurrent database operations

The Activity API provides read access to the activity log, which tracks all entity lifecycle events (created, updated, deleted) and optionally read/query operations.

Configuration

Activity tracking is controlled by these settings:

Setting Type Default Description
ACTIVITY_RETENTION_DAYS int None Days to keep events (None = forever). Cleanup happens lazily on API access.
ACTIVITY_TRACK_READS bool false Track read/query operations (opt-in, can be high volume)

GET /api/v1/activity

List activity events with filtering and pagination.

Query Parameters:

Parameter Type Default Description
entity_type string - Filter by type: memory, project, document, code_artifact, entity, link
action string - Filter by action: created, updated, deleted, read, queried
entity_id int - Filter by specific entity ID
actor string - Filter by actor: user, system, llm-maintenance
since datetime - Only events after this timestamp (ISO 8601)
until datetime - Only events before this timestamp (ISO 8601)
limit int 50 Results per page (1-100)
offset int 0 Skip N results

Response:

{
  "events": [
    {
      "id": 123,
      "entity_type": "memory",
      "entity_id": 1,
      "action": "updated",
      "changes": {
        "title": {"old": "Old Title", "new": "New Title"},
        "importance": {"old": 5, "new": 8}
      },
      "snapshot": {
        "id": 1,
        "title": "New Title",
        "content": "...",
        "importance": 8
      },
      "actor": "user",
      "actor_id": null,
      "metadata": null,
      "created_at": "2026-01-06T12:00:00Z"
    }
  ],
  "total": 42,
  "limit": 50,
  "offset": 0
}

GET /api/v1/activity/{entity_type}/{entity_id}

Get activity history for a specific entity.

Path Parameters:

Parameter Type Description
entity_type string Entity type: memory, project, document, code_artifact, entity
entity_id int Entity ID

Query Parameters:

Parameter Type Default Description
limit int 50 Results per page (1-100)
offset int 0 Skip N results

Response: Same format as GET /api/v1/activity

Event Types

Entity Types:

  • memory - Knowledge memories
  • project - Project containers
  • document - Long-form documents
  • code_artifact - Code snippets
  • entity - People, organizations, devices
  • link - Memory-to-memory connections
  • entity_memory_link - Entity-to-memory connections
  • entity_project_link - Entity-to-project connections
  • entity_relationship - Entity-to-entity relationships

Action Types:

  • created - Entity was created
  • updated - Entity was modified (includes changes diff)
  • deleted - Entity was soft-deleted (obsolete)
  • read - Entity was read (if ACTIVITY_TRACK_READS=true)
  • queried - Search was performed (if ACTIVITY_TRACK_READS=true)

Actor Types:

  • user - Human user action
  • system - Automated system action
  • llm-maintenance - LLM-based maintenance task (future)

Changes Format

For updated events, the changes field contains a diff:

{
  "changes": {
    "field_name": {
      "old": "previous value",
      "new": "current value"
    }
  }
}

Only modified fields are included. The snapshot field contains the full entity state after the change.

Link Events

Link events (entity_type: "link") use entity_id: 0 and store source/target in metadata:

{
  "entity_type": "link",
  "entity_id": 0,
  "action": "created",
  "snapshot": {"source_id": 1, "target_id": 2},
  "metadata": {"source_id": 1, "target_id": 2}
}

GET /api/v1/activity/stream

Stream activity events in real-time via Server-Sent Events (SSE).

Events are filtered to only those belonging to the authenticated user. Each event includes a sequence number for gap detection and client recovery.

Query Parameters:

Parameter Type Description
entity_type string Filter by type (optional)
action string Filter by action (optional)

Response: SSE stream with text/event-stream content type.

Event Format:

event: activity
data: {"seq": 1, "entity_type": "memory", "action": "created", "entity_id": 42, ...}

event: activity
data: {"seq": 2, "entity_type": "memory", "action": "updated", "entity_id": 42, ...}

Sequence Numbers:

Each event includes a monotonically increasing seq field per user. Clients should track this to detect gaps (e.g., receiving seq 45 when last seen was 42 indicates missed events).

Gap Recovery:

On gap detection, fetch missed events via the REST API:

GET /api/v1/activity?since=<last_seen_timestamp>&limit=100

Configuration:

Setting Type Default Description
SSE_MAX_QUEUE_SIZE int 1000 Max events per subscriber queue (backpressure)

When the queue is full, new events are dropped with a warning log. Clients can detect this via sequence gaps and resync via REST.

Example Usage (JavaScript):

const events = new EventSource('/api/v1/activity/stream');
let lastSeq = 0;

events.addEventListener('activity', (e) => {
  const event = JSON.parse(e.data);

  // Gap detection
  if (lastSeq > 0 && event.seq > lastSeq + 1) {
    console.warn(`Gap detected: ${lastSeq} -> ${event.seq}`);
    // Fetch missed events via REST API
  }

  lastSeq = event.seq;
  handleEvent(event);
});

Error Responses:

Code Description
400 Invalid filter parameter
401 Unauthorized
503 Activity streaming not enabled (ACTIVITY_ENABLED=false)

Plans

GET /api/v1/plans

List plans with optional filtering.

Query Parameters:

Parameter Type Description
project_id int Filter by project
external_ref string Exact, user-scoped reference lookup
status string Filter by status: draft, active, completed, archived

Response:

{
  "plans": [
    {
      "id": 1,
      "title": "Plan Title",
      "project_id": 1,
      "status": "draft",
      "external_ref": "github:ScottRBK/factory#42",
      "task_count": 3,
      "created_at": "2024-12-05T10:00:00Z",
      "updated_at": "2024-12-05T10:00:00Z"
    }
  ],
  "total": 5
}

GET /api/v1/plans/{plan_id}

Get a single plan by ID.

Response:

{
  "id": 1,
  "title": "Plan Title",
  "project_id": 1,
  "goal": "Implement feature X",
  "context": "Background context for the plan",
  "status": "draft",
  "external_ref": "github:ScottRBK/factory#42",
  "task_count": 3,
  "created_at": "2024-12-05T10:00:00Z",
  "updated_at": "2024-12-05T10:00:00Z"
}

POST /api/v1/plans

Create a new plan.

Request Body:

{
  "title": "Plan Title",
  "project_id": 1,
  "goal": "Optional goal description",
  "context": "Optional background context",
  "status": "draft"
}
Field Type Required Description
title string yes Plan title
project_id int yes Parent project ID
goal string no Goal or objective
context string no Background context
external_ref string no Opaque reference unique across your plans; default null
status string no Initial status (default: draft). Values: draft, active, completed, archived

Response (201): Full plan object (see GET response above)

Reference rules: external_ref is trimmed, case-sensitive, and must contain 1–255 characters when supplied. Multiple null references are allowed. Uniqueness covers every project and status owned by the authenticated user; other users may reuse the same value. source_url remains independent provenance, and existing plans receive no automatic reference backfill.

Errors: 400 for invalid references; 404 when project_id is not one of your projects; 409 when a reference already belongs to one of your plans. Concurrent duplicate requests cannot both succeed. After a conflict, look up the reference alone, without project or status filters, to include completed and archived plans. URL-encode the query value (especially #, which otherwise starts a URL fragment):

GET /api/v1/plans?external_ref=github%3AScottRBK%2Ffactory%2342

The response contains zero or one summary, including external_ref. Blank or overlength lookup values return 400. Additional filters are combined with the reference filter.

PUT /api/v1/plans/{plan_id}

Update an existing plan.

Request Body: (all fields optional)

{
  "title": "Updated Title",
  "goal": "Updated goal",
  "context": "Updated context",
  "status": "active"
}
Field Type Description
title string Updated title
goal string Updated goal
context string Updated context
external_ref string Replacement reference; omitted or null leaves it unchanged
status string New status (must be a valid transition)

References cannot be cleared: empty or whitespace-only values are invalid. Replacing a reference or deleting its plan releases the old value. Completing or archiving a plan retains it. Duplicate updates return 409 and leave the whole plan unchanged.

Plan Status Transitions:

From Allowed To
draft active, archived
active completed, archived
completed active (reopen)
archived active (unarchive)

Response: Updated plan object

Error Responses:

Code Condition
400 Invalid reference
404 Plan not found
409 External reference already used by one of your plans
422 Invalid status transition

DELETE /api/v1/plans/{plan_id}

Delete a plan. Cascades to all tasks, criteria, and dependencies under the plan.

Response:

{
  "success": true
}

Tasks

GET /api/v1/tasks

Query tasks for a plan. The plan_id query parameter is required.

Query Parameters:

Parameter Type Required Description
plan_id int yes Filter by plan
state string no Filter by state: todo, doing, waiting, done, cancelled
priority string no Filter by priority: P0, P1, P2, P3
assigned_agent string no Filter by assigned agent

Response:

{
  "tasks": [
    {
      "id": 1,
      "title": "Task Title",
      "plan_id": 1,
      "state": "todo",
      "priority": "P2",
      "assigned_agent": null,
      "version": 1,
      "criteria_met": 0,
      "criteria_total": 2,
      "blocked": false,
      "created_at": "2024-12-05T10:00:00Z",
      "updated_at": "2024-12-05T10:00:00Z"
    }
  ],
  "total": 10
}

GET /api/v1/tasks/{task_id}

Get a single task with its criteria and dependency IDs.

Response:

{
  "id": 1,
  "plan_id": 1,
  "title": "Task Title",
  "description": "Detailed description",
  "state": "todo",
  "priority": "P2",
  "assigned_agent": null,
  "version": 1,
  "criteria": [
    {
      "id": 1,
      "task_id": 1,
      "description": "Acceptance criterion",
      "met": false,
      "met_at": null,
      "created_at": "2024-12-05T10:00:00Z",
      "updated_at": "2024-12-05T10:00:00Z"
    }
  ],
  "dependency_ids": [2, 3],
  "created_at": "2024-12-05T10:00:00Z",
  "updated_at": "2024-12-05T10:00:00Z"
}

POST /api/v1/tasks

Create a new task with optional inline criteria and dependencies.

Request Body:

{
  "title": "Task Title",
  "plan_id": 1,
  "description": "Optional description",
  "priority": "P2",
  "assigned_agent": "agent-1",
  "criteria": [
    {"description": "Criterion 1"},
    {"description": "Criterion 2"}
  ],
  "dependency_ids": [2, 3]
}
Field Type Required Description
title string yes Task title
plan_id int yes Parent plan ID
description string no Detailed description
priority string no Priority level (default: P2). Values: P0 (critical), P1 (high), P2 (medium), P3 (low)
assigned_agent string no Agent assigned to the task
criteria array no Inline acceptance criteria to create
dependency_ids array no IDs of tasks this task depends on

Response (201): Full task object (see GET response above)

Error Responses:

Code Condition
400 Cyclic dependency detected
404 Plan or dependency task not found
422 Invalid state transition

PUT /api/v1/tasks/{task_id}

Update task metadata. State changes must go through the transition endpoint.

Request Body: (all fields optional)

{
  "title": "Updated Title",
  "description": "Updated description",
  "priority": "P1"
}
Field Type Description
title string Updated title
description string Updated description
priority string Updated priority: P0, P1, P2, P3

Response: Updated task object

DELETE /api/v1/tasks/{task_id}

Delete a task.

Response:

{
  "success": true
}

POST /api/v1/tasks/{task_id}/transition

Transition a task to a new state. Uses optimistic concurrency via the version field -- the request is rejected if the provided version does not match the current version.

Send a JSON object with the documented field types. Versions must be JSON integers; booleans, fractional or integral floats, and numeric strings are rejected rather than coerced.

Request Body:

{
  "state": "doing",
  "version": 1
}
Field Type Required Description
state string yes Target state: todo, doing, waiting, done, cancelled
version int yes Current version for optimistic concurrency

Task State Transitions:

From Allowed To
todo doing, waiting, cancelled
doing done, waiting, todo, cancelled
waiting todo, doing, cancelled
done todo (reopen)
cancelled todo (reinstate)

Response: Updated task object (version is incremented on success)

Error Responses:

Code Condition
400 Malformed JSON, non-object body, or missing/invalid fields
404 Task not found
409 Version mismatch (optimistic concurrency conflict)
422 Invalid state transition, or unmet dependencies block the transition

POST /api/v1/tasks/{task_id}/claim

Claim a task by assigning an agent and transitioning it to doing. Uses optimistic concurrency via the version field.

Send a JSON object with a string agent_id and a JSON integer version. Other values are rejected rather than converted to an identifier or version.

Request Body:

{
  "agent_id": "agent-1",
  "version": 1
}
Field Type Required Description
agent_id string yes Identifier of the claiming agent
version int yes Current version for optimistic concurrency

Response: Updated task object (state set to doing, assigned_agent set, version incremented)

Error Responses:

Code Condition
400 Malformed JSON, non-object body, or missing/invalid fields
404 Task not found
409 Version mismatch (optimistic concurrency conflict)
422 Invalid state transition, or unmet dependencies block the claim

Criteria

POST /api/v1/tasks/{task_id}/criteria

Add an acceptance criterion to a task.

Request Body:

{
  "description": "Acceptance criterion description"
}
Field Type Required Description
description string yes Criterion description

Response (201):

{
  "id": 1,
  "task_id": 1,
  "description": "Acceptance criterion description",
  "met": false,
  "met_at": null,
  "created_at": "2024-12-05T10:00:00Z",
  "updated_at": "2024-12-05T10:00:00Z"
}

Error Responses:

Code Condition
404 Task not found
422 Task is in a state that does not allow adding criteria

PUT /api/v1/criteria/{criterion_id}

Update a criterion (e.g., mark it as met).

Request Body: (all fields optional)

{
  "description": "Updated description",
  "met": true
}
Field Type Description
description string Updated description
met bool Whether the criterion has been met

Response: Updated criterion object

DELETE /api/v1/criteria/{criterion_id}

Delete an acceptance criterion.

Response:

{
  "success": true
}

Dependencies

POST /api/v1/tasks/{task_id}/dependencies

Add a dependency, declaring that task_id depends on another task.

Send a JSON object with a JSON integer depends_on_task_id. Booleans, floats, and numeric strings are rejected rather than converted to a different task ID.

Request Body:

{
  "depends_on_task_id": 2
}
Field Type Required Description
depends_on_task_id int yes ID of the task that must complete first

Response (201):

{
  "id": 1,
  "task_id": 1,
  "depends_on_task_id": 2,
  "created_at": "2024-12-05T10:00:00Z"
}

Error Responses:

Code Condition
400 Malformed JSON, non-object body, missing/invalid fields, cyclic dependency, or task depends on itself
404 Task not found

DELETE /api/v1/tasks/{task_id}/dependencies/{depends_on_task_id}

Remove a dependency between two tasks.

Response:

{
  "success": true
}

Error Responses

All error responses follow this format:

{
  "error": "Error message describing what went wrong"
}

For validation errors (400), the error may contain detailed field-level errors:

{
  "error": [
    {
      "loc": ["body", "title"],
      "msg": "field required",
      "type": "value_error.missing"
    }
  ]
}