Plan2Agent Memory Server는 로컬 P2A 산출물을 관계형으로 저장하고 검색하기 위한 headless REST service입니다. 로컬 파일이 원본(source of truth)이고, 이 서버는 동기화된 artifact의 canonical ID, lineage, hash, relation, keyword/vector 검색 인덱스를 제공하는 보조 저장소입니다.
서버는 P2A harness, agent, 외부 AI API를 실행하지 않습니다. Embedding 값은 외부 클라이언트가 생성해 주입하며, 서버는 받은 embedding을 embedding_sets와 chunk_embeddings에 저장하고 검색합니다.
- Java 21
- Gradle wrapper
- Docker Compose
- Docker가 실행 가능한 로컬 환경
compose.yaml은 pgvector/pgvector:pg17 기반 PostgreSQL을 시작합니다.
docker compose up -d postgres기본 DB 설정은 다음과 같습니다.
- DB:
p2a_artifact_store - User:
p2a - Password:
p2a_local_password - Port:
5432 - JDBC URL:
jdbc:postgresql://localhost:5432/p2a_artifact_store
기본값으로 실행:
./gradlew bootRun환경 변수로 DB와 인증을 지정할 수 있습니다.
P2A_DB_URL=jdbc:postgresql://localhost:5432/p2a_artifact_store \
P2A_DB_USERNAME=p2a \
P2A_DB_PASSWORD=p2a_local_password \
P2A_LOCAL_TOKEN=local-dev-token \
./gradlew bootRunP2A_LOCAL_TOKEN이 비어 있으면 /api/**도 인증 없이 열립니다. 값이 있으면 /api/health와 /actuator/health를 제외한 /api/** 요청에 X-P2A-Local-Token header가 필요합니다. Header 이름은 P2A_LOCAL_TOKEN_HEADER로 바꿀 수 있습니다.
curl http://localhost:8080/actuator/health
curl http://localhost:8080/api/health정상 응답은 status: "UP"입니다.
통합 테스트는 Testcontainers로 pgvector/pgvector:pg16 PostgreSQL을 시작합니다.
./gradlew test
./gradlew compileKotlin compileTestKotlinLima Docker socket을 쓰는 로컬 환경에서는 다음처럼 실행할 수 있습니다.
DOCKER_HOST=unix:///Users/qoo10/.lima/default/sock/docker.sock \
TESTCONTAINERS_RYUK_DISABLED=true \
./gradlew test --rerun-tasks보호 대상:
/api/**
인증 제외:
/api/health/actuator/health
인증이 켜진 경우 요청 예:
curl -H 'X-P2A-Local-Token: local-dev-token' \
http://localhost:8080/api/artifacts인증 실패는 401과 RestErrorResponse를 반환합니다.
{
"error": "auth_error",
"message": "Missing or invalid local API token",
"status": 401
}Base URL은 기본 실행 기준 http://localhost:8080입니다. 인증이 켜져 있다면 /api/health를 제외한 /api/** 요청에 X-P2A-Local-Token header를 포함해야 합니다.
| Method | Endpoint | 설명 | 인증 |
|---|---|---|---|
POST |
/api/projects |
프로젝트를 등록하거나 upsert합니다. | 필요 |
POST |
/api/projects/{projectId}/iterations |
프로젝트에 iteration을 연결해 등록하거나 upsert합니다. | 필요 |
POST |
/api/documents/snapshots |
문서 또는 산출물 snapshot을 저장합니다. | 필요 |
POST |
/api/task-graphs |
task graph JSON과 graph metadata를 저장합니다. | 필요 |
POST |
/api/tasks/bulk |
task graph에 속한 task 목록을 bulk 저장합니다. | 필요 |
POST |
/api/runs |
task 실행 기록을 저장합니다. | 필요 |
POST |
/api/document-chunks/bulk |
문서 chunk와 선택적 embedding을 bulk 저장합니다. | 필요 |
GET |
/api/artifacts |
저장된 artifact를 filter 조건으로 조회합니다. | 필요 |
GET |
/api/search/keyword |
RAG/history lookup을 위한 keyword 검색을 수행합니다. | 필요 |
POST |
/api/search/vector |
외부 query embedding으로 vector 검색을 수행합니다. | 필요 |
GET |
/api/health |
간단한 API health check입니다. | 불필요 |
GET |
/actuator/health |
Spring Actuator health check입니다. | 불필요 |
| 항목 | 의미 |
|---|---|
| Canonical server ID | 서버가 canonical하게 다루는 ID입니다. 예: projectId, iterationId, documentId, taskGraphId, taskId, runId, chunkId. |
| Source ID | 로컬/P2A 원본 시스템의 ID입니다. 예: sourceProjectId, sourceIterationId, sourceDocumentId, sourceTaskGraphId, sourceTaskId, sourceRunId. |
| Lineage | artifact의 출처와 버전을 추적하는 metadata입니다. 예: lineage.projectId, lineage.iterationId, lineage.sourcePath, lineage.contentHash, lineage.snapshotVersion, lineage.taskId, lineage.runId. |
| Source reference | canonical ID와 원본 위치를 연결합니다. 예: sourceReference.canonicalServerId, sourceReference.uri, sourceReference.path. |
P2A GUI/CLI는 위 metadata를 사용해 git client처럼 status, diff, push, pull, conflict resolution, history UI/workflow를 구현하는 동기화 클라이언트입니다. 서버는 metadata를 저장하고 조회할 뿐, 로컬 파일을 자동 수정하거나 병합하지 않습니다.
프로젝트를 등록 또는 upsert합니다.
| Request field | 필수 | 설명 |
|---|---|---|
projectId |
선택 | Canonical server UUID입니다. |
sourceProjectId |
권장 | 로컬/P2A project ID입니다. |
name |
필수 | 프로젝트 이름입니다. |
canonicalServerId |
선택 | 생략하면 projectId를 사용합니다. |
rootPath |
선택 | 로컬 repository/project root path입니다. |
sourceReference |
선택 | 원본 위치 참조 정보입니다. |
metadata |
선택 | 확장 metadata입니다. |
{
"projectId": "11111111-1111-1111-1111-111111111111",
"sourceProjectId": "local-project",
"name": "Local Project",
"canonicalServerId": "11111111-1111-1111-1111-111111111111",
"rootPath": "/repo/local-project",
"sourceReference": {
"canonicalServerId": "11111111-1111-1111-1111-111111111111",
"uri": "file:///repo/local-project",
"path": "projects/local-project"
},
"metadata": {}
}| Response | 포함 정보 |
|---|---|
ProjectResponse |
projectId, canonicalServerId, sourceProjectId, rootPath, sourceReference, metadata |
Iteration을 프로젝트에 연결해 등록 또는 upsert합니다.
| Request field | 필수 | 설명 |
|---|---|---|
projectId |
필수 | Path variable입니다. |
iterationId |
선택 | Canonical iteration ID입니다. |
sourceIterationId |
권장 | 로컬/P2A iteration ID입니다. |
label |
필수 | Iteration 표시 이름입니다. |
status |
필수 | PLANNED, ACTIVE, APPROVED, COMPLETED, ARCHIVED 중 하나입니다. |
sourceReference |
선택 | 원본 위치 참조 정보입니다. |
metadata |
선택 | 확장 metadata입니다. |
| Response | 포함 정보 |
|---|---|
IterationResponse |
Iteration canonical/source ID, label, status, sourceReference, metadata |
문서 또는 산출물 snapshot을 저장합니다.
| Request field | 필수 | 설명 |
|---|---|---|
documentId |
선택 | Canonical document snapshot ID입니다. |
projectId |
필수 | 소속 project ID입니다. |
iterationId |
선택 | 소속 iteration ID입니다. |
sourceDocumentId |
권장 | 로컬/P2A document ID입니다. |
sourcePath |
필수 | 정규화 대상 source path입니다. |
snapshotVersion |
선택 | Snapshot version입니다. |
artifactType |
필수 | 예: DOCUMENT_SNAPSHOT. |
title |
선택 | 문서 제목입니다. |
content |
필수 | 문서 본문입니다. |
contentHash |
필수 | 내용 hash입니다. |
sourceReference |
선택 | 원본 위치 참조 정보입니다. |
capturedAt |
선택 | Snapshot 수집 시각입니다. |
metadata |
선택 | 확장 metadata입니다. |
| Response | 포함 정보 |
|---|---|
DocumentSnapshotResponse |
Snapshot 정보와 lineage.contentHash, lineage.snapshotVersion, metadata.sourceDocumentId |
Task graph JSON과 graph metadata를 저장합니다.
| Request field | 필수 | 설명 |
|---|---|---|
taskGraphId |
선택 | Canonical task graph ID입니다. |
projectId |
필수 | 소속 project ID입니다. |
iterationId |
선택 | 소속 iteration ID입니다. |
sourceTaskGraphId |
권장 | 로컬/P2A task graph ID입니다. |
sourceDocumentId |
선택 | Graph를 생성한 source document ID입니다. |
graphHash |
필수 | Graph JSON hash입니다. |
graphJson |
필수 | Task graph 원본 JSON입니다. |
taskIds |
선택 | Graph에 포함된 task ID 목록입니다. |
dependencyEdges |
선택 | Task dependency edge 목록입니다. |
sourceReference |
선택 | 원본 위치 참조 정보입니다. |
metadata |
선택 | 확장 metadata입니다. |
| Response | 포함 정보 |
|---|---|
TaskGraphResponse |
Task graph canonical/source ID, graph hash, task/dependency metadata |
Task graph에 속한 task 목록을 저장합니다.
| Request field | 필수 | 설명 |
|---|---|---|
graphId |
필수 | Task들이 속한 graph ID입니다. |
tasks[].taskId |
선택 | Canonical task ID입니다. |
tasks[].projectId |
필수 | 소속 project ID입니다. |
tasks[].iterationId |
선택 | 소속 iteration ID입니다. |
tasks[].taskGraphId |
필수 | 소속 task graph ID입니다. |
tasks[].sourceTaskId |
권장 | 로컬/P2A task ID입니다. |
tasks[].title |
필수 | Task 제목입니다. |
tasks[].description |
선택 | Task 설명입니다. |
tasks[].status |
필수 | READY, BLOCKED, IN_PROGRESS, DONE 중 하나입니다. |
tasks[].targetArea |
선택 | 구현/검토 대상 영역입니다. |
tasks[].dependencies |
선택 | 선행 task ID 목록입니다. |
tasks[].acceptanceCriteria |
선택 | 완료 기준 목록입니다. |
tasks[].sourceReference |
선택 | 원본 위치 참조 정보입니다. |
tasks[].metadata |
선택 | 확장 metadata입니다. |
| Response | 포함 정보 |
|---|---|
TaskResponse[] |
저장된 task 목록과 lineage/source metadata |
Task 실행 기록을 저장합니다.
| Request field | 필수 | 설명 |
|---|---|---|
runId |
선택 | Canonical run ID입니다. |
projectId |
필수 | 소속 project ID입니다. |
iterationId |
선택 | 소속 iteration ID입니다. |
taskId |
필수 | 실행 대상 task ID입니다. |
sourceRunId |
권장 | 로컬/P2A run ID입니다. |
status |
필수 | STARTED, FINISHED, FAILED, BLOCKED 중 하나입니다. |
agentTool |
선택 | 실행한 agent/tool 이름입니다. |
runJson |
선택 | 실행 상세 JSON입니다. |
artifactRefs |
선택 | 실행 중 생성/참조한 artifact 목록입니다. |
startedAt |
선택 | 시작 시각입니다. |
finishedAt |
선택 | 종료 시각입니다. |
sourceReference |
선택 | 원본 위치 참조 정보입니다. |
metadata |
선택 | 확장 metadata입니다. |
| Response | 포함 정보 |
|---|---|
RunRecordResponse |
Run 정보와 lineage.taskId, lineage.runId, metadata.sourceRunId |
문서 chunk와 선택적 embedding을 저장합니다. embeddingSet과 embedding은 함께 제공해야 하며, 둘 중 하나만 있으면 validation error입니다.
| Request field | 필수 | 설명 |
|---|---|---|
documentId |
필수 | Chunk가 속한 document ID입니다. |
chunks[].chunk.chunkId |
선택 | Canonical chunk ID입니다. |
chunks[].chunk.projectId |
필수 | 소속 project ID입니다. |
chunks[].chunk.iterationId |
선택 | 소속 iteration ID입니다. |
chunks[].chunk.taskId |
선택 | 연결된 task ID입니다. |
chunks[].chunk.runId |
선택 | 연결된 run ID입니다. |
chunks[].chunk.artifactType |
필수 | Chunk의 artifact type입니다. |
chunks[].chunk.sourcePath |
필수 | 정규화 대상 source path입니다. |
chunks[].chunk.chunkIndex |
필수 | 문서 내 chunk 순서입니다. |
chunks[].chunk.content |
필수 | Chunk 본문입니다. |
chunks[].chunk.chunkHash |
필수 | Chunk 내용 hash입니다. |
chunks[].chunk.tokenEstimate |
선택 | Token 추정치입니다. |
chunks[].chunk.sourceReference |
선택 | 원본 위치 참조 정보입니다. |
chunks[].chunk.metadata |
선택 | Chunk 확장 metadata입니다. |
chunks[].embeddingSet |
선택 | Embedding set 정보입니다. |
chunks[].embedding |
선택 | 외부 클라이언트가 생성한 vector입니다. |
chunks[].embeddingHash |
선택 | Embedding vector hash입니다. |
embeddingSet field |
필수 | 설명 |
|---|---|---|
embeddingSetId |
선택 | Canonical embedding set ID입니다. |
projectId |
필수 | 소속 project ID입니다. |
embeddingModel |
필수 | Embedding model 이름입니다. |
embeddingDimension |
필수 | Vector 차원입니다. |
embeddingVersion |
필수 | Embedding model/version 문자열입니다. |
distanceMetric |
필수 | COSINE, L2, INNER_PRODUCT 중 하나입니다. |
storageType |
필수 | VECTOR_INDEX, INLINE, EXTERNAL 중 하나입니다. |
| Response | 포함 정보 |
|---|---|
DocumentChunkResponse[] |
저장된 chunk 목록과 chunkHash, lineage.taskId, lineage.runId, sourceReference |
저장된 artifact를 filter 조건으로 조회합니다.
| Query param | 필수 | 설명 |
|---|---|---|
projectId |
선택 | Project ID filter입니다. |
iterationId |
선택 | Iteration ID filter입니다. |
sourceProjectId |
선택 | Source project ID filter입니다. |
sourceIterationId |
선택 | Source iteration ID filter입니다. |
sourceDocumentId |
선택 | Source document ID filter입니다. |
sourceTaskGraphId |
선택 | Source task graph ID filter입니다. |
sourceTaskId |
선택 | Source task ID filter입니다. |
sourceRunId |
선택 | Source run ID filter입니다. |
artifactType |
선택 | Artifact type filter입니다. |
sourcePath |
선택 | 정규화된 source path filter입니다. |
taskId |
선택 | Canonical task ID filter입니다. |
runId |
선택 | Canonical run ID filter입니다. |
contentHash |
선택 | Content hash filter입니다. |
sourceReferenceCanonicalServerId |
선택 | Source reference canonical server ID filter입니다. |
sourceReferenceUri |
선택 | Source reference URI filter입니다. |
limit |
선택 | 최대 응답 개수입니다. |
| Response | 포함 정보 |
|---|---|
ArtifactLookupResponse[] |
각 항목의 lineage, sourceIds, sourceReference, contentHash, snapshotVersion |
RAG/history lookup을 위한 deterministic lexical retrieval입니다.
| Query param | 필수 | 설명 |
|---|---|---|
q |
필수 | Keyword query입니다. |
projectId |
선택 | Project ID filter입니다. |
iterationId |
선택 | Iteration ID filter입니다. |
artifactType |
선택 | Artifact type filter입니다. |
sourcePath |
선택 | 정규화된 source path filter입니다. |
taskId |
선택 | Canonical task ID filter입니다. |
runId |
선택 | Canonical run ID filter입니다. |
limit |
선택 | 최대 응답 개수입니다. |
| 검색 동작 | 설명 |
|---|---|
| 검색 대상 | 주 대상은 document_chunks.content, 보조 대상은 documents.content, sourcePath, artifactType입니다. |
| Matching | Case-insensitive matching입니다. |
| Filter semantics | 여러 filter는 AND semantics로 적용됩니다. |
| Score | score는 backend-opaque 값이며 API 안정 계약으로 고정하지 않습니다. |
| Tie-break | 동률은 snapshot/version/timestamp/chunkIndex 기준으로 정렬합니다. |
| Response | 포함 정보 |
|---|---|
KeywordSearchResponse[] |
content, score, matchReason, lineage, sourceIds, metadata |
외부에서 받은 query embedding으로 pgvector exact search를 수행합니다.
| Request field | 필수 | 설명 |
|---|---|---|
embedding |
필수 | Query vector입니다. 비어 있으면 validation error입니다. |
embeddingModel |
필수 | 검색할 embedding model 이름입니다. |
embeddingDimension |
필수 | Vector 차원입니다. embedding.size와 같아야 합니다. |
embeddingVersion |
필수 | 검색할 embedding version입니다. |
distanceMetric |
선택 | 생략 시 COSINE입니다. |
projectId |
선택 | Project ID filter입니다. |
iterationId |
선택 | Iteration ID filter입니다. |
artifactType |
선택 | Artifact type filter입니다. |
sourcePath |
선택 | 정규화된 source path filter입니다. |
taskId |
선택 | Canonical task ID filter입니다. |
runId |
선택 | Canonical run ID filter입니다. |
metadataFilters |
선택 | Metadata key/value filter입니다. |
limit |
선택 | 최대 응답 개수입니다. |
| 검색 동작 | 설명 |
|---|---|
| Matching scope | 같은 embeddingModel, embeddingDimension, embeddingVersion, distanceMetric embedding set 안에서만 검색합니다. |
| Dimension validation | 저장된 embedding set 차원과 요청 차원이 다르면 validation error입니다. |
| Search mode | pgvector exact search를 사용합니다. |
| Response | 포함 정보 |
|---|---|
VectorSearchResponse[] |
score, distanceMetric, embeddingModel, embeddingVersion, lineage, sourceIds |
| Method | Endpoint | 설명 | 인증 |
|---|---|---|---|
GET |
/api/health |
간단한 API health endpoint입니다. | 불필요 |
GET |
/actuator/health |
Spring Actuator health endpoint입니다. | 불필요 |
동일 logical scope에서 같은 sourcePath, artifactType, contentHash가 반복 저장되면 기존 snapshot을 반환합니다. 새 row를 만들지 않습니다.
같은 sourcePath, artifactType에 다른 contentHash가 저장되면 overwrite하지 않고 새 snapshot을 만들며 snapshotVersion이 증가합니다.
같은 documentId, chunkHash가 반복 저장되면 기존 chunk를 반환합니다. 새 chunk row를 만들지 않습니다.
같은 batch 안에서 chunkId, chunkHash, chunkIndex가 중복되면 validation error입니다.
같은 chunkId와 embeddingSetId에 같은 embeddingHash와 같은 vector가 반복 저장되면 idempotent하게 기존 row를 반환합니다.
같은 chunkId와 embeddingSetId에 다른 embeddingHash 또는 다른 vector를 저장하려 하면 conflict입니다. 서버는 명시적 overwrite/update 정책 없이 기존 embedding을 덮어쓰지 않습니다.
새 embedding model 또는 version으로 전환할 때는 새 embedding_sets row와 새 chunk_embeddings row를 추가해 점진 전환과 비교 평가가 가능하게 합니다.
sourcePath는 저장 use case에서 정규화됩니다.
- 앞뒤 공백 제거
- Windows separator
\를/로 변환 - 중복
/축약 - 앞의
./제거
이 문서에서 normalizedPath는 정규화 후의 path를 의미합니다. REST 응답의 sourcePath와 DB 컬럼 source_path에는 normalizedPath가 저장됩니다.
이 문서에서 rawSourcePath는 클라이언트가 보낸 원본 path를 의미합니다. REST payload에는 별도 top-level rawSourcePath field가 없고, sourceReference.path가 원본 path 역할을 합니다. 서버는 sourceReference.path를 DB 컬럼 raw_source_path에 보존합니다.
문서와 chunk 응답의 sourcePath는 정규화된 path입니다. /api/artifacts, /api/search/keyword, /api/search/vector의 sourcePath filter도 normalizedPath 기준으로 비교됩니다. 클라이언트가 로컬 파일과 다시 매칭할 때는 sourcePath, artifactType, contentHash, snapshotVersion, sourceReference를 함께 사용해야 합니다.
공통 error response는 RestErrorResponse입니다.
{
"error": "validation_error",
"message": "field is required",
"status": 400
}대표 status:
400 validation_error: 필수 field 누락, 잘못된 enum, 잘못된 embedding dimension, 빈 query401 auth_error: local API token 누락 또는 불일치404 not_found: relation id 또는 source id를 찾을 수 없음409 conflict: 같은 logical key가 다른 canonical entity로 충돌하거나 embedding overwrite가 발생함
- 로컬 md/json 파일이 원본입니다.
- 서버는 동기화된 artifact의 저장, 조회, 검색, lineage metadata 제공을 담당합니다.
- 서버는 로컬 파일을 자동 merge/delete하지 않습니다.
- 서버는 P2A harness를 실행하지 않습니다.
- 서버는 agent를 실행하지 않습니다.
- 서버는 외부 AI API를 호출하지 않습니다.
- 서버는 embedding을 생성하지 않습니다.
- 서버 내장 웹 UI는 제공하지 않습니다.
- status, diff, push, pull, conflict resolution, history UX는 P2A GUI/CLI가 담당합니다.