Browse Source

feat: complete platform services and remove version concepts

Jax Docker 1 month ago
parent
commit
67fe1657c6
100 changed files with 4411 additions and 755 deletions
  1. 0 3
      .gitignore
  2. 10 10
      README.md
  3. 26 15
      deployments/docker/.env.example
  4. 144 41
      deployments/docker/docker-compose.yml
  5. 2 28
      deployments/docker/postgres-init/002_service_databases.sql
  6. 0 3
      deployments/docker/postgres-init/003_service_extensions.sql
  7. 1 2
      libs/core-db/src/core_db/__init__.py
  8. 1 4
      libs/core-db/src/core_db/mixins.py
  9. 6 7
      libs/core-db/src/core_db/session.py
  10. 8 16
      libs/core-domain/src/core_domain/__init__.py
  11. 2 6
      libs/core-domain/src/core_domain/agent_contracts.py
  12. 1 1
      libs/core-domain/src/core_domain/agent_tool_invocation_contracts.py
  13. 1 1
      libs/core-domain/src/core_domain/knowledge_contracts.py
  14. 4 4
      libs/core-domain/src/core_domain/runtime_contracts.py
  15. 0 12
      libs/core-domain/src/core_domain/skill_contracts.py
  16. 3 7
      libs/core-domain/src/core_domain/team_contracts.py
  17. 3 4
      libs/core-domain/src/core_domain/tool_contracts.py
  18. 1 4
      libs/core-domain/src/core_domain/workflow_contracts.py
  19. 7 2
      libs/core-shared/src/core_shared/config.py
  20. 11 2
      libs/core-shared/src/core_shared/redis_primitives.py
  21. 41 0
      libs/core-shared/src/core_shared/task_queue.py
  22. 62 3
      scripts/migrate_all.py
  23. 1 1
      services/agent-service/alembic.ini
  24. 15 2
      services/agent-service/alembic/env.py
  25. 22 0
      services/agent-service/alembic/versions/20260429_9001_remove_agent_versioning.py
  26. 112 24
      services/agent-service/app/api/routes.py
  27. 102 110
      services/agent-service/app/application/services.py
  28. 0 1
      services/agent-service/app/bootstrap/settings.py
  29. 2 2
      services/agent-service/app/db/models/__init__.py
  30. 5 10
      services/agent-service/app/db/models/agent_config.py
  31. 3 3
      services/agent-service/app/db/models/agent_definition.py
  32. 4 4
      services/agent-service/app/db/models/agent_run.py
  33. 4 4
      services/agent-service/app/db/models/agent_tool_invocation.py
  34. 55 43
      services/agent-service/app/domain/repositories.py
  35. 74 7
      services/agent-service/app/infrastructure/memory_client.py
  36. 0 3
      services/agent-service/app/infrastructure/skill_client.py
  37. 2 2
      services/agent-service/app/infrastructure/tool_client.py
  38. 41 15
      services/agent-service/app/schemas/agent.py
  39. 1 2
      services/api-gateway/alembic.ini
  40. 15 2
      services/api-gateway/alembic/env.py
  41. 22 0
      services/api-gateway/alembic/versions/20260429_9001_remove_version_columns.py
  42. 24 0
      services/api-gateway/app/api/routes.py
  43. 0 1
      services/api-gateway/app/bootstrap/settings.py
  44. 2 2
      services/api-gateway/app/db/models/api_key.py
  45. 2 2
      services/api-gateway/app/db/models/gateway_request_audit.py
  46. 8 0
      services/api-gateway/app/schemas/gateway.py
  47. 1 1
      services/auth-service/alembic.ini
  48. 14 3
      services/auth-service/alembic/env.py
  49. 78 56
      services/auth-service/alembic/versions/20260425_0001_init_auth_models.py
  50. 10 2
      services/auth-service/alembic/versions/20260427_0002_add_user_password_hash.py
  51. 0 3
      services/auth-service/alembic/versions/20260427_0003_remove_auth_partition_columns.py
  52. 64 39
      services/auth-service/alembic/versions/20260428_0004_add_identity_contract_tables.py
  53. 22 0
      services/auth-service/alembic/versions/20260429_9001_remove_version_columns.py
  54. 10 3
      services/auth-service/app/api/identity_routes.py
  55. 155 1
      services/auth-service/app/application/services.py
  56. 1 3
      services/auth-service/app/bootstrap/settings.py
  57. 2 2
      services/auth-service/app/db/models/api_key.py
  58. 2 2
      services/auth-service/app/db/models/role.py
  59. 2 2
      services/auth-service/app/db/models/role_assignment.py
  60. 2 2
      services/auth-service/app/db/models/role_permission_binding.py
  61. 2 2
      services/auth-service/app/db/models/user.py
  62. 1 1
      services/event-service/alembic.ini
  63. 15 2
      services/event-service/alembic/env.py
  64. 22 0
      services/event-service/alembic/versions/20260429_9001_remove_version_columns.py
  65. 40 0
      services/event-service/app/api/routes.py
  66. 0 1
      services/event-service/app/bootstrap/settings.py
  67. 3 3
      services/event-service/app/db/models/event_record.py
  68. 14 0
      services/event-service/app/schemas/event.py
  69. 1 1
      services/human-service/alembic.ini
  70. 15 2
      services/human-service/alembic/env.py
  71. 22 0
      services/human-service/alembic/versions/20260429_9001_remove_version_columns.py
  72. 54 0
      services/human-service/app/api/routes.py
  73. 0 1
      services/human-service/app/bootstrap/settings.py
  74. 3 3
      services/human-service/app/db/models/human_task.py
  75. 19 0
      services/human-service/app/schemas/human.py
  76. 1 1
      services/knowledge-service/alembic.ini
  77. 15 2
      services/knowledge-service/alembic/env.py
  78. 9 12
      services/knowledge-service/alembic/versions/20260427_0002_add_pgvector_embeddings.py
  79. 22 0
      services/knowledge-service/alembic/versions/20260429_9001_remove_version_columns.py
  80. 405 79
      services/knowledge-service/app/api/routes.py
  81. 11 0
      services/knowledge-service/app/application/document_parsers.py
  82. 1004 22
      services/knowledge-service/app/application/services.py
  83. 25 1
      services/knowledge-service/app/bootstrap/app.py
  84. 14 1
      services/knowledge-service/app/bootstrap/settings.py
  85. 3 3
      services/knowledge-service/app/db/models/knowledge_base.py
  86. 20 5
      services/knowledge-service/app/db/models/knowledge_chunk.py
  87. 3 3
      services/knowledge-service/app/db/models/knowledge_document.py
  88. 183 12
      services/knowledge-service/app/domain/repositories.py
  89. 1 0
      services/knowledge-service/app/infrastructure/__init__.py
  90. 298 0
      services/knowledge-service/app/infrastructure/object_storage.py
  91. 377 1
      services/knowledge-service/app/schemas/knowledge.py
  92. 176 0
      services/knowledge-service/app/worker.py
  93. 1 0
      services/knowledge-service/pyproject.toml
  94. 1 1
      services/memory-service/alembic.ini
  95. 15 2
      services/memory-service/alembic/env.py
  96. 22 0
      services/memory-service/alembic/versions/20260429_9001_remove_version_columns.py
  97. 106 54
      services/memory-service/app/api/routes.py
  98. 245 4
      services/memory-service/app/application/services.py
  99. 23 1
      services/memory-service/app/bootstrap/app.py
  100. 7 1
      services/memory-service/app/bootstrap/settings.py

+ 0 - 3
.gitignore

@@ -4,9 +4,6 @@ __pycache__/
 *.pyc
 *.pyo
 *.pyd
-*.db
-*.sqlite
-*.sqlite3
 *.egg-info/
 .pytest_cache/
 .ruff_cache/

+ 10 - 10
README.md

@@ -73,7 +73,7 @@ cd D:\workspace\auto-platform\services\api-gateway
 uvicorn app.main:app --reload --port 8000
 ```
 
-数据库连接默认使用各服务目录下的 SQLite 文件可以通过环境变量覆盖:
+数据库连接默认使用 PostgreSQL,可以通过环境变量覆盖:
 
 ```powershell
 $env:AGENT_PLATFORM_DATABASE_URL="postgresql+psycopg://user:password@localhost:5432/workflow_db"
@@ -384,7 +384,7 @@ Run a standalone team worker process:
 
 ```powershell
 Push-Location .\services\team-service
-$env:AGENT_PLATFORM_DATABASE_URL="sqlite:///./team_service.db"
+$env:AGENT_PLATFORM_DATABASE_URL="postgresql+psycopg://admin:password@git.newpoint.work:5432/vectordb"
 $env:AGENT_PLATFORM_WORKER_DRY_RUN="true"
 ..\..\.venv\Scripts\python -m app.worker
 Pop-Location
@@ -497,8 +497,8 @@ and search fall back to local hash embeddings.
 
 When running on PostgreSQL with pgvector, `knowledge_chunk.embedding_vector`
 is populated and search uses pgvector cosine similarity first, then combines it
-with keyword scoring. SQLite and other databases automatically fall back to the
-JSON embedding hybrid search path.
+with keyword scoring. PostgreSQL with pgvector is the supported retrieval
+database for this platform.
 
 Create a knowledge base:
 
@@ -623,7 +623,7 @@ Run the scheduler worker locally:
 
 ```powershell
 Push-Location .\services\scheduler-service
-$env:AGENT_PLATFORM_DATABASE_URL="sqlite:///./scheduler_service.db"
+$env:AGENT_PLATFORM_DATABASE_URL="postgresql+psycopg://admin:password@git.newpoint.work:5432/vectordb"
 $env:AGENT_PLATFORM_EVENT_SERVICE_URL="http://127.0.0.1:8013"
 python -m app.worker
 Pop-Location
@@ -685,7 +685,7 @@ Run a standalone agent worker process:
 
 ```powershell
 Push-Location .\services\agent-service
-$env:AGENT_PLATFORM_DATABASE_URL="sqlite:///./agent_service.db"
+$env:AGENT_PLATFORM_DATABASE_URL="postgresql+psycopg://admin:password@git.newpoint.work:5432/vectordb"
 $env:AGENT_PLATFORM_WORKER_DRY_RUN="true"
 ..\..\.venv\Scripts\python -m app.worker
 Pop-Location
@@ -746,12 +746,12 @@ Run a standalone runtime worker process:
 
 ```powershell
 Push-Location .\services\runtime-service
-$env:AGENT_PLATFORM_DATABASE_URL="sqlite:///./runtime_service.db"
+$env:AGENT_PLATFORM_DATABASE_URL="postgresql+psycopg://admin:password@git.newpoint.work:5432/vectordb"
 ..\..\.venv\Scripts\python -m app.worker
 Pop-Location
 ```
 
-The worker uses `node_run.status` plus `lease_expire_time` as a DB-backed queue. This keeps the first scalable version dependency-light; for heavier production concurrency, move `AGENT_PLATFORM_DATABASE_URL` to PostgreSQL before scaling many workers.
+The worker uses `node_run.status` plus `lease_expire_time` as a DB-backed queue, with PostgreSQL and Redis as the supported scaling baseline.
 
 Node execution results are now persisted on `node_run`:
 
@@ -1286,8 +1286,8 @@ docker compose -f .\deployments\docker\docker-compose.yml down
 
 Important notes:
 
-- Services still fall back to SQLite files under `/data` if `AGENT_PLATFORM_DATABASE_URL` is not set.
-- For scaled workers, use PostgreSQL plus Redis rather than SQLite.
+- Services default to PostgreSQL; set `AGENT_PLATFORM_DATABASE_URL` explicitly for each environment.
+- Scaled workers should use PostgreSQL plus Redis for locks, queues, idempotency, and leases.
 - `core-shared.redis_primitives` provides `DistributedLock`, `IdempotencyStore`, and `RedisQueue` for services that need cross-process coordination.
 - `agent-worker`, `runtime-worker`, and `scheduler-worker` use Redis locks/idempotency when Redis is available, and fall back to DB leases when Redis is not available.
 - `agent-service` stores agent definitions, prompt/config versions, and agent run records under `/data`

+ 26 - 15
deployments/docker/.env.example

@@ -4,22 +4,23 @@ AGENT_PLATFORM_DEFAULT_MODEL=gpt-4o-mini
 AGENT_PLATFORM_POSTGRES_USER=admin
 AGENT_PLATFORM_POSTGRES_PASSWORD=hFOvG5UBeK5KIGhz5cQH
 AGENT_PLATFORM_POSTGRES_DB=vectordb
-AGENT_PLATFORM_WORKFLOW_DATABASE_URL=postgresql+psycopg://admin:hFOvG5UBeK5KIGhz5cQH@postgres:5432/workflow_service
-AGENT_PLATFORM_SESSION_DATABASE_URL=postgresql+psycopg://admin:hFOvG5UBeK5KIGhz5cQH@postgres:5432/session_service
-AGENT_PLATFORM_RUNTIME_DATABASE_URL=postgresql+psycopg://admin:hFOvG5UBeK5KIGhz5cQH@postgres:5432/runtime_service
-AGENT_PLATFORM_TOOL_DATABASE_URL=postgresql+psycopg://admin:hFOvG5UBeK5KIGhz5cQH@postgres:5432/tool_service
-AGENT_PLATFORM_MODEL_GATEWAY_DATABASE_URL=postgresql+psycopg://admin:hFOvG5UBeK5KIGhz5cQH@postgres:5432/model_gateway
-AGENT_PLATFORM_AGENT_DATABASE_URL=postgresql+psycopg://admin:hFOvG5UBeK5KIGhz5cQH@postgres:5432/agent_service
-AGENT_PLATFORM_MEMORY_DATABASE_URL=postgresql+psycopg://admin:hFOvG5UBeK5KIGhz5cQH@postgres:5432/memory_service
-AGENT_PLATFORM_TEAM_DATABASE_URL=postgresql+psycopg://admin:hFOvG5UBeK5KIGhz5cQH@postgres:5432/team_service
-AGENT_PLATFORM_SKILL_DATABASE_URL=postgresql+psycopg://admin:hFOvG5UBeK5KIGhz5cQH@postgres:5432/skill_service
-AGENT_PLATFORM_HUMAN_DATABASE_URL=postgresql+psycopg://admin:hFOvG5UBeK5KIGhz5cQH@postgres:5432/human_service
-AGENT_PLATFORM_KNOWLEDGE_DATABASE_URL=postgresql+psycopg://admin:hFOvG5UBeK5KIGhz5cQH@postgres:5432/knowledge_service
-AGENT_PLATFORM_EVENT_DATABASE_URL=postgresql+psycopg://admin:hFOvG5UBeK5KIGhz5cQH@postgres:5432/event_service
+AGENT_PLATFORM_WORKFLOW_DATABASE_URL=postgresql+psycopg://admin:hFOvG5UBeK5KIGhz5cQH@git.newpoint.work:5432/vectordb
+AGENT_PLATFORM_SESSION_DATABASE_URL=postgresql+psycopg://admin:hFOvG5UBeK5KIGhz5cQH@git.newpoint.work:5432/vectordb
+AGENT_PLATFORM_RUNTIME_DATABASE_URL=postgresql+psycopg://admin:hFOvG5UBeK5KIGhz5cQH@git.newpoint.work:5432/vectordb
+AGENT_PLATFORM_TOOL_DATABASE_URL=postgresql+psycopg://admin:hFOvG5UBeK5KIGhz5cQH@git.newpoint.work:5432/vectordb
+AGENT_PLATFORM_MODEL_GATEWAY_DATABASE_URL=postgresql+psycopg://admin:hFOvG5UBeK5KIGhz5cQH@git.newpoint.work:5432/vectordb
+AGENT_PLATFORM_AGENT_DATABASE_URL=postgresql+psycopg://admin:hFOvG5UBeK5KIGhz5cQH@git.newpoint.work:5432/vectordb
+AGENT_PLATFORM_MEMORY_DATABASE_URL=postgresql+psycopg://admin:hFOvG5UBeK5KIGhz5cQH@git.newpoint.work:5432/vectordb
+AGENT_PLATFORM_TEAM_DATABASE_URL=postgresql+psycopg://admin:hFOvG5UBeK5KIGhz5cQH@git.newpoint.work:5432/vectordb
+AGENT_PLATFORM_SKILL_DATABASE_URL=postgresql+psycopg://admin:hFOvG5UBeK5KIGhz5cQH@git.newpoint.work:5432/vectordb
+AGENT_PLATFORM_HUMAN_DATABASE_URL=postgresql+psycopg://admin:hFOvG5UBeK5KIGhz5cQH@git.newpoint.work:5432/vectordb
+AGENT_PLATFORM_KNOWLEDGE_DATABASE_URL=postgresql+psycopg://admin:hFOvG5UBeK5KIGhz5cQH@git.newpoint.work:5432/vectordb
+AGENT_PLATFORM_EVENT_DATABASE_URL=postgresql+psycopg://admin:hFOvG5UBeK5KIGhz5cQH@git.newpoint.work:5432/vectordb
 AGENT_PLATFORM_AUTH_DATABASE_URL=postgresql+psycopg://admin:hFOvG5UBeK5KIGhz5cQH@git.newpoint.work:5432/vectordb
-AGENT_PLATFORM_SCHEDULER_DATABASE_URL=postgresql+psycopg://admin:hFOvG5UBeK5KIGhz5cQH@postgres:5432/scheduler_service
-AGENT_PLATFORM_API_GATEWAY_DATABASE_URL=postgresql+psycopg://admin:hFOvG5UBeK5KIGhz5cQH@postgres:5432/api_gateway
-AGENT_PLATFORM_REDIS_URL=redis://redis:6379/0
+AGENT_PLATFORM_SCHEDULER_DATABASE_URL=postgresql+psycopg://admin:hFOvG5UBeK5KIGhz5cQH@git.newpoint.work:5432/vectordb
+AGENT_PLATFORM_API_GATEWAY_DATABASE_URL=postgresql+psycopg://admin:hFOvG5UBeK5KIGhz5cQH@git.newpoint.work:5432/vectordb
+AGENT_PLATFORM_REDIS_PASSWORD=H1v6U8uTMnByJ0SO
+AGENT_PLATFORM_REDIS_URL=redis://:H1v6U8uTMnByJ0SO@git.newpoint.work:6379/0
 AGENT_PLATFORM_EMBEDDING_PROVIDER=local
 AGENT_PLATFORM_EMBEDDING_BASE_URL=
 AGENT_PLATFORM_EMBEDDING_API_KEY=
@@ -28,6 +29,12 @@ AGENT_PLATFORM_RETRIEVAL_KEYWORD_WEIGHT=0.55
 AGENT_PLATFORM_RETRIEVAL_VECTOR_WEIGHT=0.30
 AGENT_PLATFORM_RETRIEVAL_RERANK_WEIGHT=0.15
 AGENT_PLATFORM_RETRIEVAL_RERANK_ENABLED=true
+AGENT_PLATFORM_MINIO_ROOT_USER=minioadmin
+AGENT_PLATFORM_MINIO_ROOT_PASSWORD=minioadmin
+AGENT_PLATFORM_KNOWLEDGE_OBJECT_STORAGE_BUCKET=agent-platform-knowledge
+AGENT_PLATFORM_KNOWLEDGE_OBJECT_STORAGE_ENDPOINT_URL=http://minio:9000
+AGENT_PLATFORM_KNOWLEDGE_OBJECT_STORAGE_REGION=us-east-1
+AGENT_PLATFORM_KNOWLEDGE_OBJECT_STORAGE_PATH_STYLE=true
 AGENT_PLATFORM_MAX_TIMEOUT_SECONDS=30
 AGENT_PLATFORM_AUTH_REQUIRED=true
 AGENT_PLATFORM_AUTHZ_REQUIRED=false
@@ -39,6 +46,10 @@ AGENT_PLATFORM_GLOBAL_RATE_LIMIT_PER_MINUTE=600
 AGENT_PLATFORM_API_KEY_RATE_LIMIT_PER_MINUTE=1200
 AGENT_PLATFORM_WORKER_POLL_INTERVAL_SECONDS=1
 AGENT_PLATFORM_WORKER_LEASE_SECONDS=300
+AGENT_PLATFORM_KNOWLEDGE_WORKER_STALE_INDEXING_SECONDS=600
+AGENT_PLATFORM_MEMORY_SEARCH_CACHE_TTL_SECONDS=30
+AGENT_PLATFORM_MCP_DISCOVERY_TIMEOUT_SECONDS=5
+AGENT_PLATFORM_TOOL_WORKER_STALE_DISCOVERY_SECONDS=300
 AGENT_PLATFORM_SCHEDULER_WORKER_CLAIM_LIMIT=20
 AGENT_PLATFORM_AGENT_WORKER_DRY_RUN=false
 AGENT_PLATFORM_TEAM_WORKER_DRY_RUN=true

+ 144 - 41
deployments/docker/docker-compose.yml

@@ -2,6 +2,7 @@ x-agent-platform-common-env: &agent-platform-common-env
   AGENT_PLATFORM_INTERNAL_SERVICE_AUTH_REQUIRED: ${AGENT_PLATFORM_INTERNAL_SERVICE_AUTH_REQUIRED:-false}
   AGENT_PLATFORM_INTERNAL_SERVICE_TOKEN: ${AGENT_PLATFORM_INTERNAL_SERVICE_TOKEN:-}
   AGENT_PLATFORM_CREDENTIAL_ENCRYPTION_KEY: ${AGENT_PLATFORM_CREDENTIAL_ENCRYPTION_KEY:-local-development-credential-key}
+  AGENT_PLATFORM_REDIS_URL: ${AGENT_PLATFORM_REDIS_URL:-redis://:H1v6U8uTMnByJ0SO@git.newpoint.work:6379/0}
 
 services:
   postgres:
@@ -18,25 +19,59 @@ services:
       - postgres_data:/var/lib/postgresql/data
       - ./postgres-init:/docker-entrypoint-initdb.d:ro
     healthcheck:
-      test: ["CMD-SHELL", "pg_isready -U ${AGENT_PLATFORM_POSTGRES_USER:-agent_platform} -d ${AGENT_PLATFORM_POSTGRES_DB:-agent_platform}"]
+      test: ["CMD-SHELL", "pg_isready -U ${AGENT_PLATFORM_POSTGRES_USER:-admin} -d ${AGENT_PLATFORM_POSTGRES_DB:-vectordb}"]
       interval: 10s
       timeout: 5s
       retries: 10
 
   redis:
-    image: redis:7-alpine
-    container_name: agent-platform-redis
-    command: ["redis-server", "--appendonly", "yes"]
+    image: redis:latest
+    container_name: redis
+    restart: unless-stopped
+    command: ["sh", "-c", "redis-server --appendonly yes --requirepass \"$$REDIS_PASSWORD\""]
+    environment:
+      REDIS_PASSWORD: ${AGENT_PLATFORM_REDIS_PASSWORD:-H1v6U8uTMnByJ0SO}
     ports:
       - "6379:6379"
     volumes:
       - redis_data:/data
     healthcheck:
-      test: ["CMD", "redis-cli", "ping"]
+      test: ["CMD-SHELL", "redis-cli -a \"$$REDIS_PASSWORD\" ping | grep PONG"]
       interval: 10s
       timeout: 5s
       retries: 10
 
+  minio:
+    image: minio/minio:RELEASE.2025-04-22T22-12-26Z
+    container_name: agent-platform-minio
+    command: ["server", "/data", "--console-address", ":9001"]
+    environment:
+      MINIO_ROOT_USER: ${AGENT_PLATFORM_MINIO_ROOT_USER:-minioadmin}
+      MINIO_ROOT_PASSWORD: ${AGENT_PLATFORM_MINIO_ROOT_PASSWORD:-minioadmin}
+    ports:
+      - "9000:9000"
+      - "9001:9001"
+    volumes:
+      - minio_data:/data
+
+  minio-init:
+    image: minio/mc:RELEASE.2025-04-16T18-13-26Z
+    depends_on:
+      minio:
+        condition: service_started
+    entrypoint:
+      - /bin/sh
+      - -c
+      - |
+        until mc alias set local http://minio:9000 "$$MINIO_ROOT_USER" "$$MINIO_ROOT_PASSWORD"; do
+          sleep 1
+        done
+        mc mb --ignore-existing "local/$$KNOWLEDGE_BUCKET"
+    environment:
+      MINIO_ROOT_USER: ${AGENT_PLATFORM_MINIO_ROOT_USER:-minioadmin}
+      MINIO_ROOT_PASSWORD: ${AGENT_PLATFORM_MINIO_ROOT_PASSWORD:-minioadmin}
+      KNOWLEDGE_BUCKET: ${AGENT_PLATFORM_KNOWLEDGE_OBJECT_STORAGE_BUCKET:-agent-platform-knowledge}
+
   prometheus:
     image: prom/prometheus:v2.54.1
     container_name: agent-platform-prometheus
@@ -92,8 +127,7 @@ services:
     command: ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8002"]
     environment:
       <<: *agent-platform-common-env
-      AGENT_PLATFORM_DATABASE_URL: ${AGENT_PLATFORM_WORKFLOW_DATABASE_URL:-sqlite:////data/workflow_service.db}
-      AGENT_PLATFORM_REDIS_URL: ${AGENT_PLATFORM_REDIS_URL:-redis://redis:6379/0}
+      AGENT_PLATFORM_DATABASE_URL: ${AGENT_PLATFORM_WORKFLOW_DATABASE_URL:-postgresql+psycopg://admin:hFOvG5UBeK5KIGhz5cQH@git.newpoint.work:5432/vectordb}
     ports:
       - "8002:8002"
     volumes:
@@ -114,8 +148,7 @@ services:
     command: ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8001"]
     environment:
       <<: *agent-platform-common-env
-      AGENT_PLATFORM_DATABASE_URL: ${AGENT_PLATFORM_SESSION_DATABASE_URL:-sqlite:////data/session_service.db}
-      AGENT_PLATFORM_REDIS_URL: ${AGENT_PLATFORM_REDIS_URL:-redis://redis:6379/0}
+      AGENT_PLATFORM_DATABASE_URL: ${AGENT_PLATFORM_SESSION_DATABASE_URL:-postgresql+psycopg://admin:hFOvG5UBeK5KIGhz5cQH@git.newpoint.work:5432/vectordb}
       AGENT_PLATFORM_RUNTIME_SERVICE_URL: http://runtime-service:8003
     ports:
       - "8001:8001"
@@ -140,8 +173,7 @@ services:
     command: ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8004"]
     environment:
       <<: *agent-platform-common-env
-      AGENT_PLATFORM_DATABASE_URL: ${AGENT_PLATFORM_TOOL_DATABASE_URL:-sqlite:////data/tool_service.db}
-      AGENT_PLATFORM_REDIS_URL: ${AGENT_PLATFORM_REDIS_URL:-redis://redis:6379/0}
+      AGENT_PLATFORM_DATABASE_URL: ${AGENT_PLATFORM_TOOL_DATABASE_URL:-postgresql+psycopg://admin:hFOvG5UBeK5KIGhz5cQH@git.newpoint.work:5432/vectordb}
     ports:
       - "8004:8004"
     volumes:
@@ -152,6 +184,26 @@ services:
       timeout: 5s
       retries: 5
 
+  tool-worker:
+    build:
+      context: ../..
+      dockerfile: deployments/docker/python-service.Dockerfile
+      args:
+        SERVICE_PATH: services/tool-service
+    command: ["python", "-m", "app.worker"]
+    environment:
+      <<: *agent-platform-common-env
+      AGENT_PLATFORM_DATABASE_URL: ${AGENT_PLATFORM_TOOL_DATABASE_URL:-postgresql+psycopg://admin:hFOvG5UBeK5KIGhz5cQH@git.newpoint.work:5432/vectordb}
+      AGENT_PLATFORM_MCP_DISCOVERY_TIMEOUT_SECONDS: ${AGENT_PLATFORM_MCP_DISCOVERY_TIMEOUT_SECONDS:-5}
+      AGENT_PLATFORM_WORKER_POLL_INTERVAL_SECONDS: ${AGENT_PLATFORM_WORKER_POLL_INTERVAL_SECONDS:-1}
+      AGENT_PLATFORM_WORKER_LEASE_SECONDS: ${AGENT_PLATFORM_WORKER_LEASE_SECONDS:-120}
+      AGENT_PLATFORM_WORKER_STALE_DISCOVERY_SECONDS: ${AGENT_PLATFORM_TOOL_WORKER_STALE_DISCOVERY_SECONDS:-300}
+    volumes:
+      - tool_service_data:/data
+    depends_on:
+      tool-service:
+        condition: service_started
+
   model-gateway-service:
     build:
       context: ../..
@@ -162,7 +214,7 @@ services:
     command: ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8005"]
     environment:
       <<: *agent-platform-common-env
-      AGENT_PLATFORM_DATABASE_URL: ${AGENT_PLATFORM_MODEL_GATEWAY_DATABASE_URL:-sqlite:////data/model_gateway_service.db}
+      AGENT_PLATFORM_DATABASE_URL: ${AGENT_PLATFORM_MODEL_GATEWAY_DATABASE_URL:-postgresql+psycopg://admin:hFOvG5UBeK5KIGhz5cQH@git.newpoint.work:5432/vectordb}
       AGENT_PLATFORM_PROVIDER_BASE_URL: ${AGENT_PLATFORM_PROVIDER_BASE_URL:-http://host.docker.internal:11434/v1}
       AGENT_PLATFORM_PROVIDER_API_KEY: ${AGENT_PLATFORM_PROVIDER_API_KEY:-}
       AGENT_PLATFORM_DEFAULT_MODEL: ${AGENT_PLATFORM_DEFAULT_MODEL:-}
@@ -206,8 +258,7 @@ services:
     command: ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8007"]
     environment:
       <<: *agent-platform-common-env
-      AGENT_PLATFORM_DATABASE_URL: ${AGENT_PLATFORM_AGENT_DATABASE_URL:-sqlite:////data/agent_service.db}
-      AGENT_PLATFORM_REDIS_URL: ${AGENT_PLATFORM_REDIS_URL:-redis://redis:6379/0}
+      AGENT_PLATFORM_DATABASE_URL: ${AGENT_PLATFORM_AGENT_DATABASE_URL:-postgresql+psycopg://admin:hFOvG5UBeK5KIGhz5cQH@git.newpoint.work:5432/vectordb}
       AGENT_PLATFORM_MODEL_GATEWAY_SERVICE_URL: http://model-gateway-service:8005
       AGENT_PLATFORM_MEMORY_SERVICE_URL: http://memory-service:8008
       AGENT_PLATFORM_TOOL_SERVICE_URL: http://tool-service:8004
@@ -243,8 +294,7 @@ services:
     command: ["python", "-m", "app.worker"]
     environment:
       <<: *agent-platform-common-env
-      AGENT_PLATFORM_DATABASE_URL: ${AGENT_PLATFORM_AGENT_DATABASE_URL:-sqlite:////data/agent_service.db}
-      AGENT_PLATFORM_REDIS_URL: ${AGENT_PLATFORM_REDIS_URL:-redis://redis:6379/0}
+      AGENT_PLATFORM_DATABASE_URL: ${AGENT_PLATFORM_AGENT_DATABASE_URL:-postgresql+psycopg://admin:hFOvG5UBeK5KIGhz5cQH@git.newpoint.work:5432/vectordb}
       AGENT_PLATFORM_MODEL_GATEWAY_SERVICE_URL: http://model-gateway-service:8005
       AGENT_PLATFORM_MEMORY_SERVICE_URL: http://memory-service:8008
       AGENT_PLATFORM_TOOL_SERVICE_URL: http://tool-service:8004
@@ -277,8 +327,7 @@ services:
     command: ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8008"]
     environment:
       <<: *agent-platform-common-env
-      AGENT_PLATFORM_DATABASE_URL: ${AGENT_PLATFORM_MEMORY_DATABASE_URL:-sqlite:////data/memory_service.db}
-      AGENT_PLATFORM_REDIS_URL: ${AGENT_PLATFORM_REDIS_URL:-redis://redis:6379/0}
+      AGENT_PLATFORM_DATABASE_URL: ${AGENT_PLATFORM_MEMORY_DATABASE_URL:-postgresql+psycopg://admin:hFOvG5UBeK5KIGhz5cQH@git.newpoint.work:5432/vectordb}
     ports:
       - "8008:8008"
     volumes:
@@ -289,6 +338,25 @@ services:
       timeout: 5s
       retries: 5
 
+  memory-worker:
+    build:
+      context: ../..
+      dockerfile: deployments/docker/python-service.Dockerfile
+      args:
+        SERVICE_PATH: services/memory-service
+    command: ["python", "-m", "app.worker"]
+    environment:
+      <<: *agent-platform-common-env
+      AGENT_PLATFORM_DATABASE_URL: ${AGENT_PLATFORM_MEMORY_DATABASE_URL:-postgresql+psycopg://admin:hFOvG5UBeK5KIGhz5cQH@git.newpoint.work:5432/vectordb}
+      AGENT_PLATFORM_SEARCH_CACHE_TTL_SECONDS: ${AGENT_PLATFORM_MEMORY_SEARCH_CACHE_TTL_SECONDS:-30}
+      AGENT_PLATFORM_WORKER_POLL_INTERVAL_SECONDS: ${AGENT_PLATFORM_WORKER_POLL_INTERVAL_SECONDS:-1}
+      AGENT_PLATFORM_WORKER_LEASE_SECONDS: ${AGENT_PLATFORM_WORKER_LEASE_SECONDS:-120}
+    volumes:
+      - memory_service_data:/data
+    depends_on:
+      memory-service:
+        condition: service_started
+
   team-service:
     build:
       context: ../..
@@ -299,8 +367,7 @@ services:
     command: ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8009"]
     environment:
       <<: *agent-platform-common-env
-      AGENT_PLATFORM_DATABASE_URL: ${AGENT_PLATFORM_TEAM_DATABASE_URL:-sqlite:////data/team_service.db}
-      AGENT_PLATFORM_REDIS_URL: ${AGENT_PLATFORM_REDIS_URL:-redis://redis:6379/0}
+      AGENT_PLATFORM_DATABASE_URL: ${AGENT_PLATFORM_TEAM_DATABASE_URL:-postgresql+psycopg://admin:hFOvG5UBeK5KIGhz5cQH@git.newpoint.work:5432/vectordb}
       AGENT_PLATFORM_AGENT_SERVICE_URL: http://agent-service:8007
       AGENT_PLATFORM_EVENT_SERVICE_URL: http://event-service:8013
     ports:
@@ -322,8 +389,7 @@ services:
     command: ["python", "-m", "app.worker"]
     environment:
       <<: *agent-platform-common-env
-      AGENT_PLATFORM_DATABASE_URL: ${AGENT_PLATFORM_TEAM_DATABASE_URL:-sqlite:////data/team_service.db}
-      AGENT_PLATFORM_REDIS_URL: ${AGENT_PLATFORM_REDIS_URL:-redis://redis:6379/0}
+      AGENT_PLATFORM_DATABASE_URL: ${AGENT_PLATFORM_TEAM_DATABASE_URL:-postgresql+psycopg://admin:hFOvG5UBeK5KIGhz5cQH@git.newpoint.work:5432/vectordb}
       AGENT_PLATFORM_AGENT_SERVICE_URL: http://agent-service:8007
       AGENT_PLATFORM_EVENT_SERVICE_URL: http://event-service:8013
       AGENT_PLATFORM_WORKER_POLL_INTERVAL_SECONDS: ${AGENT_PLATFORM_WORKER_POLL_INTERVAL_SECONDS:-1}
@@ -347,8 +413,7 @@ services:
     command: ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8010"]
     environment:
       <<: *agent-platform-common-env
-      AGENT_PLATFORM_DATABASE_URL: ${AGENT_PLATFORM_SKILL_DATABASE_URL:-sqlite:////data/skill_service.db}
-      AGENT_PLATFORM_REDIS_URL: ${AGENT_PLATFORM_REDIS_URL:-redis://redis:6379/0}
+      AGENT_PLATFORM_DATABASE_URL: ${AGENT_PLATFORM_SKILL_DATABASE_URL:-postgresql+psycopg://admin:hFOvG5UBeK5KIGhz5cQH@git.newpoint.work:5432/vectordb}
     ports:
       - "8010:8010"
     volumes:
@@ -369,8 +434,7 @@ services:
     command: ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8011"]
     environment:
       <<: *agent-platform-common-env
-      AGENT_PLATFORM_DATABASE_URL: ${AGENT_PLATFORM_HUMAN_DATABASE_URL:-sqlite:////data/human_service.db}
-      AGENT_PLATFORM_REDIS_URL: ${AGENT_PLATFORM_REDIS_URL:-redis://redis:6379/0}
+      AGENT_PLATFORM_DATABASE_URL: ${AGENT_PLATFORM_HUMAN_DATABASE_URL:-postgresql+psycopg://admin:hFOvG5UBeK5KIGhz5cQH@git.newpoint.work:5432/vectordb}
     ports:
       - "8011:8011"
     volumes:
@@ -391,8 +455,7 @@ services:
     command: ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8012"]
     environment:
       <<: *agent-platform-common-env
-      AGENT_PLATFORM_DATABASE_URL: ${AGENT_PLATFORM_KNOWLEDGE_DATABASE_URL:-sqlite:////data/knowledge_service.db}
-      AGENT_PLATFORM_REDIS_URL: ${AGENT_PLATFORM_REDIS_URL:-redis://redis:6379/0}
+      AGENT_PLATFORM_DATABASE_URL: ${AGENT_PLATFORM_KNOWLEDGE_DATABASE_URL:-postgresql+psycopg://admin:hFOvG5UBeK5KIGhz5cQH@git.newpoint.work:5432/vectordb}
       AGENT_PLATFORM_EMBEDDING_PROVIDER: ${AGENT_PLATFORM_EMBEDDING_PROVIDER:-local}
       AGENT_PLATFORM_EMBEDDING_BASE_URL: ${AGENT_PLATFORM_EMBEDDING_BASE_URL:-}
       AGENT_PLATFORM_EMBEDDING_API_KEY: ${AGENT_PLATFORM_EMBEDDING_API_KEY:-}
@@ -401,16 +464,62 @@ services:
       AGENT_PLATFORM_RETRIEVAL_VECTOR_WEIGHT: ${AGENT_PLATFORM_RETRIEVAL_VECTOR_WEIGHT:-0.30}
       AGENT_PLATFORM_RETRIEVAL_RERANK_WEIGHT: ${AGENT_PLATFORM_RETRIEVAL_RERANK_WEIGHT:-0.15}
       AGENT_PLATFORM_RETRIEVAL_RERANK_ENABLED: ${AGENT_PLATFORM_RETRIEVAL_RERANK_ENABLED:-true}
+      AGENT_PLATFORM_OBJECT_STORAGE_BACKEND: minio
+      AGENT_PLATFORM_OBJECT_STORAGE_BUCKET: ${AGENT_PLATFORM_KNOWLEDGE_OBJECT_STORAGE_BUCKET:-agent-platform-knowledge}
+      AGENT_PLATFORM_OBJECT_STORAGE_ENDPOINT_URL: ${AGENT_PLATFORM_KNOWLEDGE_OBJECT_STORAGE_ENDPOINT_URL:-http://minio:9000}
+      AGENT_PLATFORM_OBJECT_STORAGE_ACCESS_KEY: ${AGENT_PLATFORM_MINIO_ROOT_USER:-minioadmin}
+      AGENT_PLATFORM_OBJECT_STORAGE_SECRET_KEY: ${AGENT_PLATFORM_MINIO_ROOT_PASSWORD:-minioadmin}
+      AGENT_PLATFORM_OBJECT_STORAGE_REGION: ${AGENT_PLATFORM_KNOWLEDGE_OBJECT_STORAGE_REGION:-us-east-1}
+      AGENT_PLATFORM_OBJECT_STORAGE_PATH_STYLE: ${AGENT_PLATFORM_KNOWLEDGE_OBJECT_STORAGE_PATH_STYLE:-true}
     ports:
       - "8012:8012"
     volumes:
       - knowledge_service_data:/data
+    depends_on:
+      minio-init:
+        condition: service_completed_successfully
     healthcheck:
       test: ["CMD", "python", "-c", "import urllib.request; urllib.request.urlopen('http://127.0.0.1:8012/knowledge/health').read()"]
       interval: 15s
       timeout: 5s
       retries: 5
 
+  knowledge-worker:
+    build:
+      context: ../..
+      dockerfile: deployments/docker/python-service.Dockerfile
+      args:
+        SERVICE_PATH: services/knowledge-service
+    command: ["python", "-m", "app.worker"]
+    environment:
+      <<: *agent-platform-common-env
+      AGENT_PLATFORM_DATABASE_URL: ${AGENT_PLATFORM_KNOWLEDGE_DATABASE_URL:-postgresql+psycopg://admin:hFOvG5UBeK5KIGhz5cQH@git.newpoint.work:5432/vectordb}
+      AGENT_PLATFORM_EMBEDDING_PROVIDER: ${AGENT_PLATFORM_EMBEDDING_PROVIDER:-local}
+      AGENT_PLATFORM_EMBEDDING_BASE_URL: ${AGENT_PLATFORM_EMBEDDING_BASE_URL:-}
+      AGENT_PLATFORM_EMBEDDING_API_KEY: ${AGENT_PLATFORM_EMBEDDING_API_KEY:-}
+      AGENT_PLATFORM_EMBEDDING_MODEL: ${AGENT_PLATFORM_EMBEDDING_MODEL:-local-hash-v1}
+      AGENT_PLATFORM_RETRIEVAL_KEYWORD_WEIGHT: ${AGENT_PLATFORM_RETRIEVAL_KEYWORD_WEIGHT:-0.55}
+      AGENT_PLATFORM_RETRIEVAL_VECTOR_WEIGHT: ${AGENT_PLATFORM_RETRIEVAL_VECTOR_WEIGHT:-0.30}
+      AGENT_PLATFORM_RETRIEVAL_RERANK_WEIGHT: ${AGENT_PLATFORM_RETRIEVAL_RERANK_WEIGHT:-0.15}
+      AGENT_PLATFORM_RETRIEVAL_RERANK_ENABLED: ${AGENT_PLATFORM_RETRIEVAL_RERANK_ENABLED:-true}
+      AGENT_PLATFORM_OBJECT_STORAGE_BACKEND: minio
+      AGENT_PLATFORM_OBJECT_STORAGE_BUCKET: ${AGENT_PLATFORM_KNOWLEDGE_OBJECT_STORAGE_BUCKET:-agent-platform-knowledge}
+      AGENT_PLATFORM_OBJECT_STORAGE_ENDPOINT_URL: ${AGENT_PLATFORM_KNOWLEDGE_OBJECT_STORAGE_ENDPOINT_URL:-http://minio:9000}
+      AGENT_PLATFORM_OBJECT_STORAGE_ACCESS_KEY: ${AGENT_PLATFORM_MINIO_ROOT_USER:-minioadmin}
+      AGENT_PLATFORM_OBJECT_STORAGE_SECRET_KEY: ${AGENT_PLATFORM_MINIO_ROOT_PASSWORD:-minioadmin}
+      AGENT_PLATFORM_OBJECT_STORAGE_REGION: ${AGENT_PLATFORM_KNOWLEDGE_OBJECT_STORAGE_REGION:-us-east-1}
+      AGENT_PLATFORM_OBJECT_STORAGE_PATH_STYLE: ${AGENT_PLATFORM_KNOWLEDGE_OBJECT_STORAGE_PATH_STYLE:-true}
+      AGENT_PLATFORM_WORKER_POLL_INTERVAL_SECONDS: ${AGENT_PLATFORM_WORKER_POLL_INTERVAL_SECONDS:-1}
+      AGENT_PLATFORM_WORKER_LEASE_SECONDS: ${AGENT_PLATFORM_WORKER_LEASE_SECONDS:-300}
+      AGENT_PLATFORM_WORKER_STALE_INDEXING_SECONDS: ${AGENT_PLATFORM_KNOWLEDGE_WORKER_STALE_INDEXING_SECONDS:-600}
+    volumes:
+      - knowledge_service_data:/data
+    depends_on:
+      knowledge-service:
+        condition: service_started
+      minio-init:
+        condition: service_completed_successfully
+
   event-service:
     build:
       context: ../..
@@ -421,8 +530,7 @@ services:
     command: ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8013"]
     environment:
       <<: *agent-platform-common-env
-      AGENT_PLATFORM_DATABASE_URL: ${AGENT_PLATFORM_EVENT_DATABASE_URL:-sqlite:////data/event_service.db}
-      AGENT_PLATFORM_REDIS_URL: ${AGENT_PLATFORM_REDIS_URL:-redis://redis:6379/0}
+      AGENT_PLATFORM_DATABASE_URL: ${AGENT_PLATFORM_EVENT_DATABASE_URL:-postgresql+psycopg://admin:hFOvG5UBeK5KIGhz5cQH@git.newpoint.work:5432/vectordb}
     ports:
       - "8013:8013"
     volumes:
@@ -444,7 +552,6 @@ services:
     environment:
       <<: *agent-platform-common-env
       AGENT_PLATFORM_DATABASE_URL: ${AGENT_PLATFORM_AUTH_DATABASE_URL:-postgresql+psycopg://admin:hFOvG5UBeK5KIGhz5cQH@git.newpoint.work:5432/vectordb}
-      AGENT_PLATFORM_REDIS_URL: ${AGENT_PLATFORM_REDIS_URL:-redis://redis:6379/0}
     ports:
       - "8014:8014"
     volumes:
@@ -465,8 +572,7 @@ services:
     command: ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8015"]
     environment:
       <<: *agent-platform-common-env
-      AGENT_PLATFORM_DATABASE_URL: ${AGENT_PLATFORM_SCHEDULER_DATABASE_URL:-sqlite:////data/scheduler_service.db}
-      AGENT_PLATFORM_REDIS_URL: ${AGENT_PLATFORM_REDIS_URL:-redis://redis:6379/0}
+      AGENT_PLATFORM_DATABASE_URL: ${AGENT_PLATFORM_SCHEDULER_DATABASE_URL:-postgresql+psycopg://admin:hFOvG5UBeK5KIGhz5cQH@git.newpoint.work:5432/vectordb}
     ports:
       - "8015:8015"
     volumes:
@@ -486,8 +592,7 @@ services:
     command: ["python", "-m", "app.worker"]
     environment:
       <<: *agent-platform-common-env
-      AGENT_PLATFORM_DATABASE_URL: ${AGENT_PLATFORM_SCHEDULER_DATABASE_URL:-sqlite:////data/scheduler_service.db}
-      AGENT_PLATFORM_REDIS_URL: ${AGENT_PLATFORM_REDIS_URL:-redis://redis:6379/0}
+      AGENT_PLATFORM_DATABASE_URL: ${AGENT_PLATFORM_SCHEDULER_DATABASE_URL:-postgresql+psycopg://admin:hFOvG5UBeK5KIGhz5cQH@git.newpoint.work:5432/vectordb}
       AGENT_PLATFORM_EVENT_SERVICE_URL: http://event-service:8013
       AGENT_PLATFORM_WORKER_POLL_INTERVAL_SECONDS: ${AGENT_PLATFORM_WORKER_POLL_INTERVAL_SECONDS:-1}
       AGENT_PLATFORM_WORKER_LEASE_SECONDS: ${AGENT_PLATFORM_WORKER_LEASE_SECONDS:-300}
@@ -510,8 +615,7 @@ services:
     command: ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8003"]
     environment:
       <<: *agent-platform-common-env
-      AGENT_PLATFORM_DATABASE_URL: ${AGENT_PLATFORM_RUNTIME_DATABASE_URL:-sqlite:////data/runtime_service.db}
-      AGENT_PLATFORM_REDIS_URL: ${AGENT_PLATFORM_REDIS_URL:-redis://redis:6379/0}
+      AGENT_PLATFORM_DATABASE_URL: ${AGENT_PLATFORM_RUNTIME_DATABASE_URL:-postgresql+psycopg://admin:hFOvG5UBeK5KIGhz5cQH@git.newpoint.work:5432/vectordb}
       AGENT_PLATFORM_WORKFLOW_SERVICE_URL: http://workflow-service:8002
       AGENT_PLATFORM_TOOL_SERVICE_URL: http://tool-service:8004
       AGENT_PLATFORM_MODEL_GATEWAY_SERVICE_URL: http://model-gateway-service:8005
@@ -568,8 +672,7 @@ services:
     command: ["python", "-m", "app.worker"]
     environment:
       <<: *agent-platform-common-env
-      AGENT_PLATFORM_DATABASE_URL: ${AGENT_PLATFORM_RUNTIME_DATABASE_URL:-sqlite:////data/runtime_service.db}
-      AGENT_PLATFORM_REDIS_URL: ${AGENT_PLATFORM_REDIS_URL:-redis://redis:6379/0}
+      AGENT_PLATFORM_DATABASE_URL: ${AGENT_PLATFORM_RUNTIME_DATABASE_URL:-postgresql+psycopg://admin:hFOvG5UBeK5KIGhz5cQH@git.newpoint.work:5432/vectordb}
       AGENT_PLATFORM_WORKFLOW_SERVICE_URL: http://workflow-service:8002
       AGENT_PLATFORM_TOOL_SERVICE_URL: http://tool-service:8004
       AGENT_PLATFORM_MODEL_GATEWAY_SERVICE_URL: http://model-gateway-service:8005
@@ -607,8 +710,7 @@ services:
     command: ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000"]
     environment:
       <<: *agent-platform-common-env
-      AGENT_PLATFORM_DATABASE_URL: ${AGENT_PLATFORM_API_GATEWAY_DATABASE_URL:-sqlite:////data/api_gateway.db}
-      AGENT_PLATFORM_REDIS_URL: ${AGENT_PLATFORM_REDIS_URL:-redis://redis:6379/0}
+      AGENT_PLATFORM_DATABASE_URL: ${AGENT_PLATFORM_API_GATEWAY_DATABASE_URL:-postgresql+psycopg://admin:hFOvG5UBeK5KIGhz5cQH@git.newpoint.work:5432/vectordb}
       AGENT_PLATFORM_WORKFLOW_SERVICE_URL: http://workflow-service:8002
       AGENT_PLATFORM_SESSION_SERVICE_URL: http://session-service:8001
       AGENT_PLATFORM_RUNTIME_SERVICE_URL: http://runtime-service:8003
@@ -673,6 +775,7 @@ services:
 volumes:
   postgres_data:
   redis_data:
+  minio_data:
   prometheus_data:
   api_gateway_data:
   agent_service_data:

+ 2 - 28
deployments/docker/postgres-init/002_service_databases.sql

@@ -1,28 +1,2 @@
-SELECT 'CREATE DATABASE workflow_service'
-WHERE NOT EXISTS (SELECT FROM pg_database WHERE datname = 'workflow_service')\gexec
-SELECT 'CREATE DATABASE session_service'
-WHERE NOT EXISTS (SELECT FROM pg_database WHERE datname = 'session_service')\gexec
-SELECT 'CREATE DATABASE runtime_service'
-WHERE NOT EXISTS (SELECT FROM pg_database WHERE datname = 'runtime_service')\gexec
-SELECT 'CREATE DATABASE tool_service'
-WHERE NOT EXISTS (SELECT FROM pg_database WHERE datname = 'tool_service')\gexec
-SELECT 'CREATE DATABASE agent_service'
-WHERE NOT EXISTS (SELECT FROM pg_database WHERE datname = 'agent_service')\gexec
-SELECT 'CREATE DATABASE memory_service'
-WHERE NOT EXISTS (SELECT FROM pg_database WHERE datname = 'memory_service')\gexec
-SELECT 'CREATE DATABASE team_service'
-WHERE NOT EXISTS (SELECT FROM pg_database WHERE datname = 'team_service')\gexec
-SELECT 'CREATE DATABASE skill_service'
-WHERE NOT EXISTS (SELECT FROM pg_database WHERE datname = 'skill_service')\gexec
-SELECT 'CREATE DATABASE human_service'
-WHERE NOT EXISTS (SELECT FROM pg_database WHERE datname = 'human_service')\gexec
-SELECT 'CREATE DATABASE knowledge_service'
-WHERE NOT EXISTS (SELECT FROM pg_database WHERE datname = 'knowledge_service')\gexec
-SELECT 'CREATE DATABASE event_service'
-WHERE NOT EXISTS (SELECT FROM pg_database WHERE datname = 'event_service')\gexec
-SELECT 'CREATE DATABASE auth_service'
-WHERE NOT EXISTS (SELECT FROM pg_database WHERE datname = 'auth_service')\gexec
-SELECT 'CREATE DATABASE scheduler_service'
-WHERE NOT EXISTS (SELECT FROM pg_database WHERE datname = 'scheduler_service')\gexec
-SELECT 'CREATE DATABASE api_gateway'
-WHERE NOT EXISTS (SELECT FROM pg_database WHERE datname = 'api_gateway')\gexec
+-- Single-database deployment. All services share POSTGRES_DB, currently vectordb.
+SELECT current_database() AS agent_platform_database;

+ 0 - 3
deployments/docker/postgres-init/003_service_extensions.sql

@@ -1,4 +1 @@
-\connect knowledge_service
-CREATE EXTENSION IF NOT EXISTS vector;
-\connect memory_service
 CREATE EXTENSION IF NOT EXISTS vector;

+ 1 - 2
libs/core-db/src/core_db/__init__.py

@@ -1,5 +1,5 @@
 from .base import Base
-from .mixins import AuditMixin, EntityMixin, VersionMixin
+from .mixins import AuditMixin, EntityMixin
 from .session import (
     DatabaseSettings,
     create_engine_from_settings,
@@ -12,7 +12,6 @@ __all__ = [
     "Base",
     "DatabaseSettings",
     "EntityMixin",
-    "VersionMixin",
     "create_engine_from_settings",
     "create_session_factory",
     "transaction_scope",

+ 1 - 4
libs/core-db/src/core_db/mixins.py

@@ -1,7 +1,7 @@
 from datetime import datetime
 from uuid import uuid4
 
-from sqlalchemy import DateTime, Integer, String
+from sqlalchemy import DateTime, String
 from sqlalchemy.orm import Mapped, mapped_column
 
 
@@ -20,6 +20,3 @@ class AuditMixin:
     deleted_time: Mapped[datetime | None] = mapped_column(DateTime, nullable=True)
 
 
-class VersionMixin:
-    version: Mapped[int] = mapped_column(Integer, default=1)
-

+ 6 - 7
libs/core-db/src/core_db/session.py

@@ -5,23 +5,22 @@ from pydantic import BaseModel, Field
 from sqlalchemy import Engine, create_engine
 from sqlalchemy.orm import Session, sessionmaker
 
+DEFAULT_DATABASE_URL = (
+    "postgresql+psycopg://admin:hFOvG5UBeK5KIGhz5cQH@git.newpoint.work:5432/vectordb"
+)
+
 
 class DatabaseSettings(BaseModel):
-    database_url: str = Field(default="sqlite:///./service.db")
+    database_url: str = Field(default=DEFAULT_DATABASE_URL)
     echo_sql: bool = Field(default=False)
     pool_pre_ping: bool = Field(default=True)
 
 
 def create_engine_from_settings(settings: DatabaseSettings) -> Engine:
-    connect_args: dict[str, object] = {}
-    if settings.database_url.startswith("sqlite"):
-        connect_args["check_same_thread"] = False
-
     return create_engine(
         settings.database_url,
         echo=settings.echo_sql,
-        pool_pre_ping=settings.pool_pre_ping,
-        connect_args=connect_args)
+        pool_pre_ping=settings.pool_pre_ping)
 
 
 def create_session_factory(engine: Engine) -> sessionmaker[Session]:

+ 8 - 16
libs/core-domain/src/core_domain/__init__.py

@@ -7,8 +7,7 @@ from .agent_contracts import (
     AgentSkillRefContract,
     AgentStatus,
     AgentToolRefContract,
-    AgentVersionContract,
-    AgentVersionStatus,
+    AgentConfigContract,
 )
 from .agent_tool_invocation_contracts import AgentToolInvocationContract, AgentToolInvocationStatus
 from .auth_contracts import (
@@ -76,8 +75,6 @@ from .skill_contracts import (
     SkillRunContract,
     SkillRunStatus,
     SkillStatus,
-    SkillVersionContract,
-    SkillVersionStatus,
 )
 from .team_contracts import (
     TeamDefinitionContract,
@@ -86,8 +83,7 @@ from .team_contracts import (
     TeamRunContract,
     TeamRunStatus,
     TeamStatus,
-    TeamVersionContract,
-    TeamVersionStatus,
+    TeamConfigContract,
 )
 from .tool_contracts import (
     ToolBindingContract,
@@ -95,9 +91,9 @@ from .tool_contracts import (
     ToolCredentialContract,
     ToolCredentialRevealContract,
     ToolDefinitionContract,
-    ToolVersionContract,
+    ToolConnectionContract,
 )
-from .workflow_contracts import WorkflowVersionContract
+from .workflow_contracts import WorkflowConfigContract
 
 __all__ = [
     "AgentDefinitionContract",
@@ -110,8 +106,7 @@ __all__ = [
     "AgentToolInvocationContract",
     "AgentToolInvocationStatus",
     "AgentToolRefContract",
-    "AgentVersionContract",
-    "AgentVersionStatus",
+    "AgentConfigContract",
     "PermissionCheckContract",
     "PermissionCheckResultContract",
     "RoleAssignmentContract",
@@ -163,24 +158,21 @@ __all__ = [
     "SkillRunContract",
     "SkillRunStatus",
     "SkillStatus",
-    "SkillVersionContract",
-    "SkillVersionStatus",
     "TeamDefinitionContract",
     "TeamMemberContract",
     "TeamMemberRole",
     "TeamRunContract",
     "TeamRunStatus",
     "TeamStatus",
-    "TeamVersionContract",
-    "TeamVersionStatus",
+    "TeamConfigContract",
     "ToolBindingContract",
     "ToolBindingDetailContract",
     "ToolCredentialContract",
     "ToolCredentialRevealContract",
     "ToolDefinitionContract",
-    "ToolVersionContract",
+    "ToolConnectionContract",
     "WorkflowRunStatus",
     "WorkflowRunStatusUpdateContract",
     "WorkflowRunContract",
-    "WorkflowVersionContract",
+    "WorkflowConfigContract",
 ]

+ 2 - 6
libs/core-domain/src/core_domain/agent_contracts.py

@@ -5,7 +5,6 @@ from core_shared import JSONValue
 from pydantic import BaseModel, Field
 
 AgentStatus = Literal["draft", "active", "archived"]
-AgentVersionStatus = Literal["draft", "published", "deprecated"]
 AgentRunStatus = Literal["queued", "running", "completed", "failed", "cancelled"]
 
 
@@ -55,11 +54,9 @@ class AgentDefinitionContract(BaseModel):
     created_time: datetime
 
 
-class AgentVersionContract(BaseModel):
+class AgentConfigContract(BaseModel):
     id: str
     agent_id: str
-    version_no: int
-    status: AgentVersionStatus
     role: str
     goal: str | None = None
     system_prompt: str
@@ -67,14 +64,13 @@ class AgentVersionContract(BaseModel):
     memory_policy_json: dict[str, JSONValue]
     tool_refs_json: list[dict[str, JSONValue]]
     skill_refs_json: list[dict[str, JSONValue]]
-    published_time: datetime | None = None
     created_time: datetime
 
 
 class AgentRunContract(BaseModel):
     id: str
     agent_id: str
-    agent_version_id: str
+    agent_config_id: str
     session_id: str | None = None
     input_text: str | None = None
     input_json: dict[str, JSONValue] | None = None

+ 1 - 1
libs/core-domain/src/core_domain/agent_tool_invocation_contracts.py

@@ -11,7 +11,7 @@ class AgentToolInvocationContract(BaseModel):
     id: str
     agent_run_id: str
     agent_id: str
-    agent_version_id: str
+    agent_config_id: str
     tool_code: str | None = None
     tool_binding_id: str | None = None
     status: AgentToolInvocationStatus

+ 1 - 1
libs/core-domain/src/core_domain/knowledge_contracts.py

@@ -5,7 +5,7 @@ from core_shared import JSONValue
 from pydantic import BaseModel, Field
 
 KnowledgeBaseStatus = Literal["active", "archived"]
-KnowledgeDocumentStatus = Literal["draft", "indexed", "failed", "archived"]
+KnowledgeDocumentStatus = Literal["draft", "queued", "indexing", "indexed", "failed", "archived"]
 
 
 class KnowledgeBaseContract(BaseModel):

+ 4 - 4
libs/core-domain/src/core_domain/runtime_contracts.py

@@ -16,9 +16,9 @@ class InitialNodeContract(BaseModel):
 
 class RunCreateContract(BaseModel):
     app_id: str
-    app_version_id: str
+    app_config_id: str
     workflow_id: str
-    workflow_version_id: str
+    workflow_config_id: str
     session_id: str | None = None
     parent_run_id: str | None = None
     root_run_id: str | None = None
@@ -31,9 +31,9 @@ class RunCreateContract(BaseModel):
 class WorkflowRunContract(BaseModel):
     id: str
     app_id: str
-    app_version_id: str
+    app_config_id: str
     workflow_id: str
-    workflow_version_id: str
+    workflow_config_id: str
     session_id: str | None = None
     parent_run_id: str | None = None
     root_run_id: str | None = None

+ 0 - 12
libs/core-domain/src/core_domain/skill_contracts.py

@@ -5,7 +5,6 @@ from core_shared import JSONValue
 from pydantic import BaseModel, Field
 
 SkillStatus = Literal["draft", "active", "archived"]
-SkillVersionStatus = Literal["draft", "published", "deprecated"]
 SkillInstallStatus = Literal["installed", "disabled", "uninstalled"]
 SkillRunStatus = Literal["queued", "running", "completed", "failed", "cancelled"]
 
@@ -18,27 +17,17 @@ class SkillDefinitionContract(BaseModel):
     description: str | None = None
     status: SkillStatus
     owner_user_id: str | None = None
-    created_time: datetime
-
-
-class SkillVersionContract(BaseModel):
-    id: str
-    skill_id: str
-    version_no: int
-    status: SkillVersionStatus
     runtime_type: str
     entrypoint: str | None = None
     parameter_schema_json: dict[str, JSONValue]
     output_schema_json: dict[str, JSONValue]
     implementation_json: dict[str, JSONValue]
-    published_time: datetime | None = None
     created_time: datetime
 
 
 class SkillInstallationContract(BaseModel):
     id: str
     skill_id: str
-    skill_version_id: str
     install_scope: str
     scope_id: str
     status: SkillInstallStatus
@@ -51,7 +40,6 @@ class SkillInstallationContract(BaseModel):
 class SkillRunContract(BaseModel):
     id: str
     skill_id: str
-    skill_version_id: str
     installation_id: str | None = None
     status: SkillRunStatus
     input_json: dict[str, JSONValue] = Field(default_factory=dict)

+ 3 - 7
libs/core-domain/src/core_domain/team_contracts.py

@@ -5,7 +5,6 @@ from core_shared import JSONValue
 from pydantic import BaseModel, Field
 
 TeamStatus = Literal["draft", "active", "archived"]
-TeamVersionStatus = Literal["draft", "published", "deprecated"]
 TeamRunStatus = Literal["queued", "running", "completed", "failed", "cancelled"]
 TeamMemberRole = Literal["supervisor", "planner", "executor", "reviewer", "specialist"]
 
@@ -13,7 +12,7 @@ TeamMemberRole = Literal["supervisor", "planner", "executor", "reviewer", "speci
 class TeamMemberContract(BaseModel):
     member_key: str
     agent_id: str
-    agent_version_id: str | None = None
+    agent_config_id: str | None = None
     role: TeamMemberRole = "specialist"
     name: str | None = None
     responsibility: str | None = None
@@ -31,23 +30,20 @@ class TeamDefinitionContract(BaseModel):
     created_time: datetime
 
 
-class TeamVersionContract(BaseModel):
+class TeamConfigContract(BaseModel):
     id: str
     team_id: str
-    version_no: int
-    status: TeamVersionStatus
     coordination_mode: str
     objective: str | None = None
     member_refs_json: list[dict[str, JSONValue]]
     policy_json: dict[str, JSONValue]
-    published_time: datetime | None = None
     created_time: datetime
 
 
 class TeamRunContract(BaseModel):
     id: str
     team_id: str
-    team_version_id: str
+    team_config_id: str
     session_id: str | None = None
     input_text: str | None = None
     input_json: dict[str, JSONValue] | None = None

+ 3 - 4
libs/core-domain/src/core_domain/tool_contracts.py

@@ -14,10 +14,9 @@ class ToolDefinitionContract(BaseModel):
     created_time: datetime
 
 
-class ToolVersionContract(BaseModel):
+class ToolConnectionContract(BaseModel):
     id: str
     tool_id: str
-    version_no: int
     input_schema_json: dict[str, JSONValue] | None = None
     output_schema_json: dict[str, JSONValue] | None = None
     invoke_config_json: dict[str, JSONValue] | None = None
@@ -29,7 +28,7 @@ class ToolVersionContract(BaseModel):
 class ToolBindingContract(BaseModel):
     id: str
     app_id: str
-    tool_version_id: str
+    tool_connection_id: str
     credential_id: str | None = None
     binding_scope: str
     enabled: bool
@@ -54,5 +53,5 @@ class ToolCredentialRevealContract(BaseModel):
 
 class ToolBindingDetailContract(BaseModel):
     binding: ToolBindingContract
-    tool_version: ToolVersionContract
+    connection: ToolConnectionContract
     tool_definition: ToolDefinitionContract

+ 1 - 4
libs/core-domain/src/core_domain/workflow_contracts.py

@@ -4,14 +4,11 @@ from core_shared import JSONValue
 from pydantic import BaseModel
 
 
-class WorkflowVersionContract(BaseModel):
+class WorkflowConfigContract(BaseModel):
     id: str
     workflow_id: str
-    version_no: int
     dsl_json: dict[str, JSONValue] | None = None
     compiled_plan_json: dict[str, JSONValue] | None = None
-    schema_version: str | None = None
     checksum: str | None = None
-    status: str
     created_time: datetime
 

+ 7 - 2
libs/core-shared/src/core_shared/config.py

@@ -1,6 +1,11 @@
 from pydantic import Field
 from pydantic_settings import BaseSettings, SettingsConfigDict
 
+DEFAULT_DATABASE_URL = (
+    "postgresql+psycopg://admin:hFOvG5UBeK5KIGhz5cQH@git.newpoint.work:5432/vectordb"
+)
+DEFAULT_REDIS_URL = "redis://:H1v6U8uTMnByJ0SO@git.newpoint.work:6379/0"
+
 
 class ServiceSettings(BaseSettings):
     service_name: str = Field(default="service")
@@ -8,8 +13,8 @@ class ServiceSettings(BaseSettings):
     service_host: str = Field(default="0.0.0.0")
     service_port: int = Field(default=8000)
     debug: bool = Field(default=True)
-    database_url: str = Field(default="sqlite:///./service.db")
-    redis_url: str = Field(default="redis://127.0.0.1:6379/0")
+    database_url: str = Field(default=DEFAULT_DATABASE_URL)
+    redis_url: str = Field(default=DEFAULT_REDIS_URL)
     echo_sql: bool = Field(default=False)
     internal_service_auth_required: bool = Field(default=False)
     internal_service_token: str | None = Field(default=None)

+ 11 - 2
libs/core-shared/src/core_shared/redis_primitives.py

@@ -1,12 +1,16 @@
+from __future__ import annotations
+
 import json
 import time
 from dataclasses import dataclass
+from typing import TYPE_CHECKING
 from uuid import uuid4
 
-from redis import Redis
-
 from core_shared.types import JSONValue
 
+if TYPE_CHECKING:
+    from redis import Redis
+
 
 @dataclass(frozen=True)
 class RedisConnectionSettings:
@@ -87,6 +91,9 @@ class IdempotencyStore:
             return {str(item_key): item_value for item_key, item_value in result.items()}
         return None
 
+    def clear(self, *, key: str) -> None:
+        self.client.delete(self._key(key))
+
     def _key(self, key: str) -> str:
         return f"{self.prefix}:{key}"
 
@@ -112,6 +119,8 @@ class RedisQueue:
 
 
 def build_redis_client(settings: RedisConnectionSettings) -> Redis:
+    from redis import Redis
+
     return Redis.from_url(settings.redis_url, decode_responses=False)
 
 

+ 41 - 0
libs/core-shared/src/core_shared/task_queue.py

@@ -12,6 +12,9 @@ AGENT_RUN_QUEUE = "agent-platform:agent-runs"
 RUNTIME_NODE_RUN_QUEUE = "agent-platform:runtime-node-runs"
 SCHEDULED_JOB_QUEUE = "agent-platform:scheduled-jobs"
 TEAM_RUN_QUEUE = "agent-platform:team-runs"
+KNOWLEDGE_DOCUMENT_QUEUE = "agent-platform:knowledge-documents"
+MEMORY_TOUCH_QUEUE = "agent-platform:memory-touches"
+TOOL_MCP_DISCOVERY_QUEUE = "agent-platform:tool-mcp-discovery"
 
 
 class TaskQueueConsumer(Protocol):
@@ -46,6 +49,44 @@ class TaskQueuePublisher:
             payload={"team_run_id": team_run_id},
         )
 
+    def publish_knowledge_document(
+        self,
+        *,
+        document_id: str,
+        action: str,
+        job_id: str | None = None,
+    ) -> bool:
+        payload: dict[str, JSONValue] = {
+            "document_id": document_id,
+            "action": action,
+        }
+        if job_id is not None:
+            payload["job_id"] = job_id
+        return self._publish(
+            queue_name=KNOWLEDGE_DOCUMENT_QUEUE,
+            payload=payload,
+        )
+
+    def publish_memory_touch(self, *, memory_ids: list[str]) -> bool:
+        return self._publish(
+            queue_name=MEMORY_TOUCH_QUEUE,
+            payload={"memory_ids": memory_ids},
+        )
+
+    def publish_tool_mcp_discovery(
+        self,
+        *,
+        connection_id: str,
+        job_id: str | None = None,
+    ) -> bool:
+        payload: dict[str, JSONValue] = {"connection_id": connection_id}
+        if job_id is not None:
+            payload["job_id"] = job_id
+        return self._publish(
+            queue_name=TOOL_MCP_DISCOVERY_QUEUE,
+            payload=payload,
+        )
+
     def _publish(self, *, queue_name: str, payload: dict[str, JSONValue]) -> bool:
         try:
             from core_shared.redis_primitives import RedisQueue

+ 62 - 3
scripts/migrate_all.py

@@ -2,6 +2,7 @@ from __future__ import annotations
 
 import argparse
 import os
+import shutil
 import subprocess
 import sys
 from dataclasses import dataclass
@@ -45,10 +46,16 @@ def main() -> int:
             print(f"{target.service_name}: {target.alembic_ini_path}")
         return 0
 
+    if args.database_url:
+        print(f"using database url: {mask_database_url(args.database_url)}", flush=True)
+
     failed_services: list[str] = []
     for target in targets:
         print(f"==> migrating {target.service_name}", flush=True)
-        result = run_alembic_upgrade(target=target, python_executable=args.python)
+        result = run_alembic_upgrade(
+            target=target,
+            python_executable=args.python,
+            database_url=args.database_url)
         if result.returncode != 0:
             failed_services.append(target.service_name)
             if not args.continue_on_error:
@@ -72,6 +79,12 @@ def parse_args() -> argparse.Namespace:
         "--python",
         default=sys.executable,
         help="Python executable to use for `python -m alembic`.")
+    parser.add_argument(
+        "--database-url",
+        default=os.environ.get("AGENT_PLATFORM_DATABASE_URL"),
+        help=(
+            "Override every service migration target with one database URL. "
+            "Defaults to AGENT_PLATFORM_DATABASE_URL when set."))
     parser.add_argument(
         "--continue-on-error",
         action="store_true",
@@ -113,10 +126,14 @@ def discover_targets(
 def run_alembic_upgrade(
     *,
     target: MigrationTarget,
-    python_executable: str) -> subprocess.CompletedProcess[str]:
+    python_executable: str,
+    database_url: str | None) -> subprocess.CompletedProcess[str]:
     env = os.environ.copy()
+    if database_url:
+        env["AGENT_PLATFORM_DATABASE_URL"] = database_url
+    env["PYTHONPATH"] = build_pythonpath(target=target, existing=env.get("PYTHONPATH"))
     result = subprocess.run(
-        [python_executable, "-m", "alembic", "upgrade", "head"],
+        [resolve_alembic_executable(python_executable), "upgrade", "head"],
         cwd=target.service_path,
         env=env,
         text=True,
@@ -124,5 +141,47 @@ def run_alembic_upgrade(
     return result
 
 
+def build_pythonpath(*, target: MigrationTarget, existing: str | None) -> str:
+    repo_root = target.service_path.parents[1]
+    paths = [target.service_path]
+    paths.extend(sorted((repo_root / "libs").glob("*/src")))
+    path_entries = [str(path) for path in paths]
+    if existing:
+        path_entries.append(existing)
+    return os.pathsep.join(path_entries)
+
+
+def resolve_alembic_executable(python_executable: str) -> str:
+    python_path = Path(python_executable)
+    candidates = [
+        python_path.parent / "Scripts" / "alembic.exe",
+        python_path.parent / "Scripts" / "alembic",
+        python_path.parent / "alembic.exe",
+        python_path.parent / "alembic",
+    ]
+    for candidate in candidates:
+        if candidate.exists():
+            return str(candidate)
+
+    resolved = shutil.which("alembic")
+    if resolved:
+        return resolved
+
+    raise FileNotFoundError(
+        "alembic executable not found. Install it with `python -m pip install alembic`.")
+
+
+def mask_database_url(database_url: str) -> str:
+    marker = "://"
+    if marker not in database_url:
+        return database_url
+    scheme, rest = database_url.split(marker, 1)
+    if "@" not in rest or ":" not in rest.split("@", 1)[0]:
+        return database_url
+    credentials, host = rest.split("@", 1)
+    username = credentials.split(":", 1)[0]
+    return f"{scheme}{marker}{username}:***@{host}"
+
+
 if __name__ == "__main__":
     raise SystemExit(main())

+ 1 - 1
services/agent-service/alembic.ini

@@ -1,7 +1,7 @@
 [alembic]
 script_location = alembic
 prepend_sys_path = .
-sqlalchemy.url = sqlite:///./agent_service.db
+sqlalchemy.url = postgresql+psycopg://admin:hFOvG5UBeK5KIGhz5cQH@git.newpoint.work:5432/vectordb
 
 [loggers]
 keys = root,sqlalchemy,alembic

+ 15 - 2
services/agent-service/alembic/env.py

@@ -1,10 +1,16 @@
+import os
 from logging.config import fileConfig
 
 from alembic import context
 from app.db.models import Base
 from sqlalchemy import engine_from_config, pool
 
+SERVICE_VERSION_TABLE = "agent_alembic_version"
+
 config = context.config
+database_url = os.getenv("AGENT_PLATFORM_DATABASE_URL")
+if database_url:
+    config.set_main_option("sqlalchemy.url", database_url.replace("%", "%%"))
 
 if config.config_file_name is not None:
     fileConfig(config.config_file_name)
@@ -14,7 +20,11 @@ target_metadata = Base.metadata
 
 def run_migrations_offline() -> None:
     url = config.get_main_option("sqlalchemy.url")
-    context.configure(url=url, target_metadata=target_metadata, literal_binds=True)
+    context.configure(
+        url=url,
+        target_metadata=target_metadata,
+        literal_binds=True,
+        version_table=SERVICE_VERSION_TABLE)
 
     with context.begin_transaction():
         context.run_migrations()
@@ -27,7 +37,10 @@ def run_migrations_online() -> None:
         poolclass=pool.NullPool)
 
     with connectable.connect() as connection:
-        context.configure(connection=connection, target_metadata=target_metadata)
+        context.configure(
+            connection=connection,
+            target_metadata=target_metadata,
+            version_table=SERVICE_VERSION_TABLE)
 
         with context.begin_transaction():
             context.run_migrations()

+ 22 - 0
services/agent-service/alembic/versions/20260429_9001_remove_agent_versioning.py

@@ -0,0 +1,22 @@
+"""Remove business version schema artifacts.
+
+Revision ID: 20260429_9001_agent
+Revises: 20260426_0003
+Create Date: 2026-04-29 00:00:00.000000
+"""
+
+from alembic import op
+
+revision: str = "20260429_9001_agent"
+down_revision: str | None = "20260426_0003"
+branch_labels = None
+depends_on = None
+
+
+def upgrade() -> None:
+    op.execute("DO $$\nBEGIN\n    IF to_regclass('agent_version') IS NOT NULL AND to_regclass('agent_config') IS NULL THEN\n        ALTER TABLE agent_version RENAME TO agent_config;\n    END IF;\n    IF EXISTS (SELECT 1 FROM information_schema.columns WHERE table_name = 'agent_run' AND column_name = 'agent_version_id') THEN\n        ALTER TABLE agent_run RENAME COLUMN agent_version_id TO agent_config_id;\n    END IF;\n    IF EXISTS (SELECT 1 FROM information_schema.columns WHERE table_name = 'agent_tool_invocation' AND column_name = 'agent_version_id') THEN\n        ALTER TABLE agent_tool_invocation RENAME COLUMN agent_version_id TO agent_config_id;\n    END IF;\nEND $$;\nALTER TABLE IF EXISTS agent_config DROP COLUMN IF EXISTS version_no;\nALTER TABLE IF EXISTS agent_config DROP COLUMN IF EXISTS status;\nALTER TABLE IF EXISTS agent_config DROP COLUMN IF EXISTS published_time;\nDO $$\nDECLARE\n    table_record record;\nBEGIN\n    FOR table_record IN\n        SELECT table_name\n        FROM information_schema.columns\n        WHERE table_schema = current_schema()\n          AND column_name = 'version'\n    LOOP\n        EXECUTE format('ALTER TABLE %I DROP COLUMN IF EXISTS version', table_record.table_name);\n    END LOOP;\nEND $$;")
+
+
+def downgrade() -> None:
+    # Business version tables and columns were intentionally removed.
+    pass

+ 112 - 24
services/agent-service/app/api/routes.py

@@ -11,20 +11,27 @@ from app.schemas.agent import (
     AgentConfigListRequest,
     AgentConfigResponse,
     AgentCreateRequest,
+    AgentDeleteRequest,
+    AgentDetailRequest,
+    AgentListRequest,
     AgentResponse,
     AgentRunCreateRequest,
     AgentRunDetailRequest,
+    AgentRunExecutePostRequest,
     AgentRunExecuteRequest,
     AgentRunExecuteResponse,
+    AgentRunListRequest,
     AgentRunResponse,
+    AgentRunStatusPostRequest,
     AgentRunStatusUpdateRequest,
     AgentStatusUpdateRequest,
+    AgentStatusPostRequest,
+    AgentToolInvocationListRequest,
     AgentUpdateRequest,
     AgentToolInvocationResponse,
-    AgentVersionCreateRequest,
-    AgentVersionResponse,
     AgentWorkerExecuteNextRequest,
     AgentWorkerExecuteNextResponse,
+    DeleteData,
 )
 
 router = APIRouter()
@@ -60,6 +67,23 @@ def list_agents(
     return [AgentResponse.from_entity(item) for item in service.list_agents()]
 
 
+@router.post("/list", response_model=list[AgentResponse])
+def list_agents_post(
+    payload: AgentListRequest,
+    service: AgentApplicationService = Depends(get_agent_application_service)) -> list[AgentResponse]:
+    return [AgentResponse.from_entity(item) for item in service.list_agents()]
+
+
+@router.post("/detail", response_model=AgentResponse)
+def detail_agent(
+    payload: AgentDetailRequest,
+    service: AgentApplicationService = Depends(get_agent_application_service)) -> AgentResponse:
+    entity = service.get_agent(agent_id=payload.agent_id)
+    if entity is None:
+        raise HTTPException(status_code=404, detail=f"agent not found: {payload.agent_id}")
+    return AgentResponse.from_entity(entity)
+
+
 @router.post("/update", response_model=AgentResponse)
 def update_agent(
     payload: AgentUpdateRequest,
@@ -81,6 +105,27 @@ def update_agent_status(
     return AgentResponse.from_entity(entity)
 
 
+@router.post("/status", response_model=AgentResponse)
+def update_agent_status_post(
+    payload: AgentStatusPostRequest,
+    service: AgentApplicationService = Depends(get_agent_application_service)) -> AgentResponse:
+    entity = service.update_agent_status(
+        agent_id=payload.agent_id,
+        payload=AgentStatusUpdateRequest(status=payload.status))
+    if entity is None:
+        raise HTTPException(status_code=404, detail=f"agent not found: {payload.agent_id}")
+    return AgentResponse.from_entity(entity)
+
+
+@router.post("/delete", response_model=DeleteData)
+def delete_agent_post(
+    payload: AgentDeleteRequest,
+    service: AgentApplicationService = Depends(get_agent_application_service)) -> DeleteData:
+    return DeleteData(
+        deleted=service.delete_agent(agent_id=payload.agent_id),
+        agent_id=payload.agent_id)
+
+
 @router.post("/configs/create", response_model=AgentConfigResponse)
 def create_agent_config(
     payload: AgentConfigCreateRequest,
@@ -98,28 +143,7 @@ def list_agent_configs(
     service: AgentApplicationService = Depends(get_agent_application_service)) -> list[AgentConfigResponse]:
     return [
         AgentConfigResponse.from_entity(item)
-        for item in service.list_agent_configs(payload)
-    ]
-
-
-@router.post("/versions", response_model=AgentVersionResponse)
-def create_agent_version(
-    payload: AgentVersionCreateRequest,
-    service: AgentApplicationService = Depends(get_agent_application_service)) -> AgentVersionResponse:
-    try:
-        entity = service.create_agent_version(payload)
-    except ValueError as exc:
-        raise HTTPException(status_code=422, detail=str(exc)) from exc
-    return AgentVersionResponse.from_entity(entity)
-
-
-@router.get("/versions", response_model=list[AgentVersionResponse])
-def list_agent_versions(
-    agent_id: str = Query(...),
-    service: AgentApplicationService = Depends(get_agent_application_service)) -> list[AgentVersionResponse]:
-    return [
-        AgentVersionResponse.from_entity(item)
-        for item in service.list_agent_versions(agent_id=agent_id)
+        for item in service.list_agent_configs(agent_id=payload.agent_id)
     ]
 
 
@@ -147,6 +171,18 @@ def list_agent_runs(
     ]
 
 
+@router.post("/runs/list", response_model=list[AgentRunResponse])
+def list_agent_runs_post(
+    payload: AgentRunListRequest,
+    service: AgentApplicationService = Depends(get_agent_application_service)) -> list[AgentRunResponse]:
+    return [
+        AgentRunResponse.from_entity(item)
+        for item in service.list_agent_runs(
+            agent_id=payload.agent_id,
+            session_id=payload.session_id)
+    ]
+
+
 @router.post("/runs/detail", response_model=AgentRunResponse)
 def get_agent_run(
     payload: AgentRunDetailRequest,
@@ -170,6 +206,19 @@ def list_agent_tool_invocations(
     ]
 
 
+@router.post(
+    "/runs/tool-invocations/list",
+    response_model=list[AgentToolInvocationResponse])
+def list_agent_tool_invocations_post(
+    payload: AgentToolInvocationListRequest,
+    service: AgentApplicationService = Depends(get_agent_application_service)) -> list[AgentToolInvocationResponse]:
+    return [
+        AgentToolInvocationResponse.from_entity(item)
+        for item in service.list_agent_tool_invocations(
+            agent_run_id=payload.agent_run_id)
+    ]
+
+
 @router.post("/runs/{agent_run_id}/status", response_model=AgentRunResponse)
 def update_agent_run_status(
     agent_run_id: str,
@@ -181,6 +230,24 @@ def update_agent_run_status(
     return AgentRunResponse.from_entity(entity)
 
 
+@router.post("/runs/status", response_model=AgentRunResponse)
+def update_agent_run_status_post(
+    payload: AgentRunStatusPostRequest,
+    service: AgentApplicationService = Depends(get_agent_application_service)) -> AgentRunResponse:
+    entity = service.update_agent_run_status(
+        agent_run_id=payload.agent_run_id,
+        payload=AgentRunStatusUpdateRequest(
+            status=payload.status,
+            worker_key=payload.worker_key,
+            output_text=payload.output_text,
+            output_json=payload.output_json,
+            error_code=payload.error_code,
+            error_message=payload.error_message))
+    if entity is None:
+        raise HTTPException(status_code=404, detail=f"agent_run not found: {payload.agent_run_id}")
+    return AgentRunResponse.from_entity(entity)
+
+
 @router.post("/runs/{agent_run_id}/execute", response_model=AgentRunExecuteResponse)
 def execute_agent_run(
     agent_run_id: str,
@@ -199,6 +266,27 @@ def execute_agent_run(
         dry_run=dry_run_value if isinstance(dry_run_value, bool) else False)
 
 
+@router.post("/runs/execute", response_model=AgentRunExecuteResponse)
+def execute_agent_run_post(
+    payload: AgentRunExecutePostRequest,
+    service: AgentApplicationService = Depends(get_agent_application_service)) -> AgentRunExecuteResponse:
+    entity = service.execute_agent_run(
+        agent_run_id=payload.agent_run_id,
+        payload=AgentRunExecuteRequest(
+            worker_key=payload.worker_key,
+            dry_run=payload.dry_run))
+    if entity is None:
+        raise HTTPException(status_code=404, detail=f"agent_run not found: {payload.agent_run_id}")
+
+    output_json = entity.output_json or {}
+    model_value = output_json.get("model")
+    dry_run_value = output_json.get("dry_run")
+    return AgentRunExecuteResponse(
+        run=AgentRunResponse.from_entity(entity),
+        model=model_value if isinstance(model_value, str) else None,
+        dry_run=dry_run_value if isinstance(dry_run_value, bool) else False)
+
+
 @router.post("/workers/execute-next", response_model=AgentWorkerExecuteNextResponse)
 def execute_next_worker_task(
     payload: AgentWorkerExecuteNextRequest,

+ 102 - 110
services/agent-service/app/application/services.py

@@ -21,12 +21,12 @@ from core_shared import JSONValue, try_build_redis_client
 from core_shared.task_queue import TaskQueuePublisher
 
 from app.bootstrap.settings import AgentServiceSettings
-from app.db.models import AgentDefinition, AgentRun, AgentToolInvocation, AgentVersion
+from app.db.models import AgentDefinition, AgentRun, AgentToolInvocation, AgentConfig
 from app.domain.repositories import (
     AgentDefinitionRepository,
     AgentRunRepository,
     AgentToolInvocationRepository,
-    AgentVersionRepository)
+    AgentConfigRepository)
 from app.infrastructure.model_gateway_client import ModelGatewayClient, ModelGatewayClientError
 from app.infrastructure.memory_client import MemoryClient, MemoryClientError
 from app.infrastructure.skill_client import SkillServiceClient, SkillServiceClientError
@@ -40,8 +40,7 @@ from app.schemas.agent import (
     AgentRunExecuteRequest,
     AgentRunStatusUpdateRequest,
     AgentStatusUpdateRequest,
-    AgentUpdateRequest,
-    AgentVersionCreateRequest)
+    AgentUpdateRequest)
 
 
 def generate_agent_code() -> str:
@@ -53,7 +52,7 @@ class AgentApplicationService:
         self,
         *,
         agent_repository: AgentDefinitionRepository,
-        agent_version_repository: AgentVersionRepository,
+        agent_config_repository: AgentConfigRepository,
         agent_run_repository: AgentRunRepository,
         agent_tool_invocation_repository: AgentToolInvocationRepository,
         model_gateway_client: ModelGatewayClient | None = None,
@@ -66,7 +65,7 @@ class AgentApplicationService:
         react_max_tool_calls: int = 10,
         react_tool_retry_count: int = 1) -> None:
         self.agent_repository = agent_repository
-        self.agent_version_repository = agent_version_repository
+        self.agent_config_repository = agent_config_repository
         self.agent_run_repository = agent_run_repository
         self.agent_tool_invocation_repository = agent_tool_invocation_repository
         self.model_gateway_client = model_gateway_client
@@ -91,6 +90,9 @@ class AgentApplicationService:
     def list_agents(self) -> list[AgentDefinition]:
         return self.agent_repository.list_all()
 
+    def get_agent(self, *, agent_id: str) -> AgentDefinition | None:
+        return self.agent_repository.get_by_id(agent_id=agent_id)
+
     def update_agent(self, payload: AgentUpdateRequest) -> AgentDefinition | None:
         return self.agent_repository.update(
             agent_id=payload.agent_id,
@@ -98,6 +100,17 @@ class AgentApplicationService:
             description=payload.description,
             metadata_json=payload.metadata_json)
 
+    def delete_agent(self, *, agent_id: str) -> bool:
+        agent = self.agent_repository.get_by_id(agent_id=agent_id)
+        if agent is None:
+            return False
+        runs = self.agent_run_repository.list_by_scope(agent_id=agent_id)
+        for run in runs:
+            self.agent_tool_invocation_repository.delete_by_run(agent_run_id=run.id)
+        self.agent_run_repository.delete_by_agent(agent_id=agent_id)
+        self.agent_config_repository.delete_by_agent(agent_id=agent_id)
+        return self.agent_repository.delete(agent_id=agent_id) is not None
+
     def update_agent_status(
         self,
         *,
@@ -107,31 +120,14 @@ class AgentApplicationService:
             agent_id=agent_id,
             status=payload.status)
 
-    def create_agent_config(self, payload: AgentConfigCreateRequest) -> AgentVersion:
-        return self.create_agent_version(
-            AgentVersionCreateRequest(
-                agent_id=payload.agent_id,
-                status="draft",
-                role=payload.role,
-                goal=payload.goal,
-                system_prompt=payload.system_prompt,
-                model_config=payload.model_config_data,
-                memory_policy=payload.memory_policy,
-                tool_refs=payload.tool_refs,
-                skill_refs=payload.skill_refs))
-
-    def list_agent_configs(self, payload: AgentConfigListRequest) -> list[AgentVersion]:
-        return self.list_agent_versions(agent_id=payload.agent_id)
-
-    def create_agent_version(self, payload: AgentVersionCreateRequest) -> AgentVersion:
+    def create_agent_config(self, payload: AgentConfigCreateRequest) -> AgentConfig:
         agent = self.agent_repository.get_by_id(
             agent_id=payload.agent_id)
         if agent is None:
             raise ValueError(f"agent not found: {payload.agent_id}")
 
-        return self.agent_version_repository.create(
+        return self.agent_config_repository.create(
             agent_id=payload.agent_id,
-            status=payload.status,
             role=payload.role,
             goal=payload.goal,
             system_prompt=payload.system_prompt,
@@ -140,19 +136,19 @@ class AgentApplicationService:
             tool_refs_json=[item.model_dump(mode="json") for item in payload.tool_refs],
             skill_refs_json=[item.model_dump(mode="json") for item in payload.skill_refs])
 
-    def list_agent_versions(self, *, agent_id: str) -> list[AgentVersion]:
-        return self.agent_version_repository.list_by_agent(agent_id=agent_id)
+    def list_agent_configs(self, *, agent_id: str) -> list[AgentConfig]:
+        return self.agent_config_repository.list_by_agent(agent_id=agent_id)
 
     def create_agent_run(self, payload: AgentRunCreateRequest) -> AgentRun:
-        agent_version = self._resolve_agent_version(
+        agent_config = self._resolve_agent_config(
             agent_id=payload.agent_id,
-            agent_version_id=payload.agent_config_id or payload.agent_version_id)
-        if agent_version is None:
+            agent_config_id=payload.agent_config_id)
+        if agent_config is None:
             raise ValueError("agent config not found")
 
         agent_run = self.agent_run_repository.create(
             agent_id=payload.agent_id,
-            agent_version_id=agent_version.id,
+            agent_config_id=agent_config.id,
             session_id=payload.session_id,
             input_text=payload.input_text,
             input_json=payload.input_json)
@@ -213,15 +209,15 @@ class AgentApplicationService:
         if agent_run is None:
             return None
 
-        agent_version = self.agent_version_repository.get_by_id(
-            agent_version_id=agent_run.agent_version_id)
-        if agent_version is None:
+        agent_config = self.agent_config_repository.get_by_id(
+            agent_config_id=agent_run.agent_config_id)
+        if agent_config is None:
             return self.agent_run_repository.update_status(
                 agent_run_id=agent_run.id,
                 status="failed",
                 worker_key=payload.worker_key,
-                error_code="agent_version_missing",
-                error_message=f"agent version not found: {agent_run.agent_version_id}")
+                error_code="agent_config_missing",
+                error_message=f"agent config not found: {agent_run.agent_config_id}")
 
         self.agent_run_repository.update_status(
             agent_run_id=agent_run.id,
@@ -230,13 +226,13 @@ class AgentApplicationService:
 
         memory_results, memory_metadata = self._read_relevant_memories(
             agent_run=agent_run,
-            agent_version=agent_version)
-        selected_tools = self._select_tool_refs(agent_run=agent_run, agent_version=agent_version)
-        selected_skills = self._select_skill_refs(agent_run=agent_run, agent_version=agent_version)
+            agent_config=agent_config)
+        selected_tools = self._select_tool_refs(agent_run=agent_run, agent_config=agent_config)
+        selected_skills = self._select_skill_refs(agent_run=agent_run, agent_config=agent_config)
         if payload.dry_run:
             messages = self._build_chat_messages(
                 agent_run=agent_run,
-                agent_version=agent_version,
+                agent_config=agent_config,
                 memory_results=memory_results,
                 capability_context=self._format_capability_plan(
                     selected_tools=selected_tools,
@@ -247,10 +243,10 @@ class AgentApplicationService:
                 worker_key=payload.worker_key,
                 output_text=self._build_dry_run_output(
                     agent_run=agent_run,
-                    agent_version=agent_version),
+                    agent_config=agent_config),
                 output_json={
                     "dry_run": True,
-                    "agent_version_id": agent_version.id,
+                    "agent_config_id": agent_config.id,
                     "message_count": len(messages),
                     "messages": [message.model_dump(mode="json") for message in messages],
                     "selected_tool_refs": [
@@ -272,10 +268,10 @@ class AgentApplicationService:
                     })
             return completed_run
 
-        if self._read_bool(agent_version.model_config_json, "react_enabled", default=False):
+        if self._read_bool(agent_config.model_config_json, "react_enabled", default=False):
             return self._execute_react_agent_run(
                 agent_run=agent_run,
-                agent_version=agent_version,
+                agent_config=agent_config,
                 payload=payload,
                 memory_results=memory_results,
                 memory_metadata=memory_metadata,
@@ -284,7 +280,7 @@ class AgentApplicationService:
 
         tool_invocations = self._invoke_selected_tools(
             agent_run=agent_run,
-            agent_version=agent_version,
+            agent_config=agent_config,
             selected_tools=selected_tools)
         skill_invocations = self._invoke_selected_skills(
             agent_run=agent_run,
@@ -292,7 +288,7 @@ class AgentApplicationService:
             worker_key=payload.worker_key)
         messages = self._build_chat_messages(
             agent_run=agent_run,
-            agent_version=agent_version,
+            agent_config=agent_config,
             memory_results=memory_results,
             capability_context=self._format_capability_results(
                 tool_invocations=tool_invocations,
@@ -314,17 +310,17 @@ class AgentApplicationService:
         try:
             response = self.model_gateway_client.create_chat_completion(
                 ChatCompletionRequestContract(
-                    model=self._read_optional_string(agent_version.model_config_json, "model"),
+                    model=self._read_optional_string(agent_config.model_config_json, "model"),
                     temperature=self._read_optional_float(
-                        agent_version.model_config_json,
+                        agent_config.model_config_json,
                         "temperature"),
                     max_tokens=self._read_optional_int(
-                        agent_version.model_config_json,
+                        agent_config.model_config_json,
                         "max_tokens"),
                     messages=messages,
                     metadata_json={
                         "agent_id": agent_run.agent_id,
-                        "agent_version_id": agent_version.id,
+                        "agent_config_id": agent_config.id,
                         "agent_run_id": agent_run.id,
                     })
             )
@@ -338,7 +334,7 @@ class AgentApplicationService:
 
         memory_write_metadata = self._write_interaction_memory(
             agent_run=agent_run,
-            agent_version=agent_version,
+            agent_config=agent_config,
             output_text=response.content)
         completed_run = self.agent_run_repository.update_status(
             agent_run_id=agent_run.id,
@@ -347,7 +343,7 @@ class AgentApplicationService:
             output_text=response.content,
             output_json={
                 "dry_run": False,
-                "agent_version_id": agent_version.id,
+                "agent_config_id": agent_config.id,
                 "model": response.model,
                 "finish_reason": response.finish_reason,
                 "usage_json": response.usage_json,
@@ -387,7 +383,7 @@ class AgentApplicationService:
                     payload_json={
                         **payload_json,
                         "agent_id": agent_run.agent_id,
-                        "agent_version_id": agent_run.agent_version_id,
+                        "agent_config_id": agent_run.agent_config_id,
                     })
             )
         except EventServiceClientError:
@@ -397,7 +393,7 @@ class AgentApplicationService:
         self,
         *,
         agent_run: AgentRun,
-        agent_version: AgentVersion,
+        agent_config: AgentConfig,
         payload: AgentRunExecuteRequest,
         memory_results: list[MemorySearchResultContract],
         memory_metadata: dict[str, JSONValue],
@@ -417,7 +413,7 @@ class AgentApplicationService:
             worker_key=payload.worker_key)
         messages = self._build_chat_messages(
             agent_run=agent_run,
-            agent_version=agent_version,
+            agent_config=agent_config,
             memory_results=memory_results,
             capability_context=self._format_react_instruction(
                 agent_run=agent_run,
@@ -429,7 +425,7 @@ class AgentApplicationService:
         tool_call_count = 0
 
         max_steps = self._read_int(
-            agent_version.model_config_json,
+            agent_config.model_config_json,
             "react_max_steps",
             default=self.react_max_steps)
         for step_index in range(max(max_steps, 1)):
@@ -437,7 +433,7 @@ class AgentApplicationService:
                 response = self.model_gateway_client.create_chat_completion(
                     self._build_chat_completion_request(
                         agent_run=agent_run,
-                        agent_version=agent_version,
+                        agent_config=agent_config,
                         messages=messages,
                         selected_tools=selected_tools)
                 )
@@ -472,7 +468,7 @@ class AgentApplicationService:
                 break
 
             max_tool_calls = self._read_int(
-                agent_version.model_config_json,
+                agent_config.model_config_json,
                 "react_max_tool_calls",
                 default=self.react_max_tool_calls)
             if tool_call_count >= max(max_tool_calls, 0):
@@ -499,7 +495,7 @@ class AgentApplicationService:
                 }
             current_invocations = self._invoke_react_tool_with_retry(
                 agent_run=agent_run,
-                agent_version=agent_version,
+                agent_config=agent_config,
                 tool_ref=matching_tools[0])
             tool_call_count += len(current_invocations)
             agent_run.input_json = original_input_json
@@ -514,7 +510,7 @@ class AgentApplicationService:
 
         memory_write_metadata = self._write_interaction_memory(
             agent_run=agent_run,
-            agent_version=agent_version,
+            agent_config=agent_config,
             output_text=final_answer)
         completed_run = self.agent_run_repository.update_status(
             agent_run_id=agent_run.id,
@@ -523,7 +519,7 @@ class AgentApplicationService:
             output_text=final_answer,
             output_json={
                 "dry_run": False,
-                "agent_version_id": agent_version.id,
+                "agent_config_id": agent_config.id,
                 "react_enabled": True,
                 "react_steps": react_steps,
                 "react_tool_call_count": tool_call_count,
@@ -594,30 +590,30 @@ class AgentApplicationService:
             return None
         return result, released_lease_count
 
-    def _resolve_agent_version(
+    def _resolve_agent_config(
         self,
         *,
         agent_id: str,
-        agent_version_id: str | None) -> AgentVersion | None:
-        if agent_version_id is not None:
-            return self.agent_version_repository.get_by_id(
-                agent_version_id=agent_version_id)
-        return self.agent_version_repository.get_latest_by_agent(
+        agent_config_id: str | None) -> AgentConfig | None:
+        if agent_config_id is not None:
+            return self.agent_config_repository.get_by_id(
+                agent_config_id=agent_config_id)
+        return self.agent_config_repository.get_latest_by_agent(
             agent_id=agent_id)
 
     def _build_chat_messages(
         self,
         *,
         agent_run: AgentRun,
-        agent_version: AgentVersion,
+        agent_config: AgentConfig,
         memory_results: list[MemorySearchResultContract] | None = None,
         capability_context: str | None = None) -> list[ChatMessageContract]:
         messages = [
-            ChatMessageContract(role="system", content=agent_version.system_prompt),
+            ChatMessageContract(role="system", content=agent_config.system_prompt),
         ]
-        if agent_version.goal:
+        if agent_config.goal:
             messages.append(
-                ChatMessageContract(role="system", content=f"Goal: {agent_version.goal}")
+                ChatMessageContract(role="system", content=f"Goal: {agent_config.goal}")
             )
         if memory_results:
             messages.append(
@@ -641,10 +637,10 @@ class AgentApplicationService:
         self,
         *,
         agent_run: AgentRun,
-        agent_version: AgentVersion) -> list[AgentToolRefContract]:
+        agent_config: AgentConfig) -> list[AgentToolRefContract]:
         input_preview = self._build_input_preview(agent_run)
         selected: list[AgentToolRefContract] = []
-        for item in agent_version.tool_refs_json:
+        for item in agent_config.tool_refs_json:
             ref = AgentToolRefContract.model_validate(item)
             if (
                 ref.required
@@ -658,10 +654,10 @@ class AgentApplicationService:
         self,
         *,
         agent_run: AgentRun,
-        agent_version: AgentVersion) -> list[AgentSkillRefContract]:
+        agent_config: AgentConfig) -> list[AgentSkillRefContract]:
         input_preview = self._build_input_preview(agent_run)
         selected: list[AgentSkillRefContract] = []
-        for item in agent_version.skill_refs_json:
+        for item in agent_config.skill_refs_json:
             ref = AgentSkillRefContract.model_validate(item)
             auto_invoke = self._read_bool(ref.config_json, "auto_invoke", default=True)
             if auto_invoke or self._matches_selection_keywords(ref.config_json, input_preview):
@@ -672,14 +668,14 @@ class AgentApplicationService:
         self,
         *,
         agent_run: AgentRun,
-        agent_version: AgentVersion,
+        agent_config: AgentConfig,
         selected_tools: list[AgentToolRefContract]) -> list[dict[str, JSONValue]]:
         invocations: list[dict[str, JSONValue]] = []
         for ref in selected_tools:
             invocation = self.agent_tool_invocation_repository.create(
                 agent_run_id=agent_run.id,
                 agent_id=agent_run.agent_id,
-                agent_version_id=agent_version.id,
+                agent_config_id=agent_config.id,
                 tool_code=ref.tool_code,
                 tool_binding_id=ref.tool_binding_id,
                 status="selected",
@@ -815,9 +811,6 @@ class AgentApplicationService:
             try:
                 created_run = self.skill_client.create_skill_run(
                     skill_id=skill_id,
-                    skill_version_id=self._read_optional_string(
-                        ref.config_json,
-                        "skill_version_id"),
                     installation_id=self._read_optional_string(
                         ref.config_json,
                         "installation_id"),
@@ -915,9 +908,9 @@ class AgentApplicationService:
                             "name": detail.tool_definition.name,
                             "description": detail.tool_definition.description,
                             "tool_type": detail.tool_definition.tool_type,
-                            "input_schema_json": detail.tool_version.input_schema_json or {},
-                            "output_schema_json": detail.tool_version.output_schema_json or {},
-                            "timeout_ms": detail.tool_version.timeout_ms,
+                            "input_schema_json": detail.connection.input_schema_json or {},
+                            "output_schema_json": detail.connection.output_schema_json or {},
+                            "timeout_ms": detail.connection.timeout_ms,
                         }
                     )
                 except ToolServiceClientError as exc:
@@ -929,17 +922,17 @@ class AgentApplicationService:
         self,
         *,
         agent_run: AgentRun,
-        agent_version: AgentVersion,
+        agent_config: AgentConfig,
         tool_ref: AgentToolRefContract) -> list[dict[str, JSONValue]]:
         retry_count = self._read_int(
-            agent_version.model_config_json,
+            agent_config.model_config_json,
             "react_tool_retry_count",
             default=self.react_tool_retry_count)
         attempts: list[dict[str, JSONValue]] = []
         for attempt_index in range(max(retry_count, 0) + 1):
             current = self._invoke_selected_tools(
                 agent_run=agent_run,
-                agent_version=agent_version,
+                agent_config=agent_config,
                 selected_tools=[tool_ref])
             for item in current:
                 item["attempt_index"] = attempt_index
@@ -1011,23 +1004,23 @@ class AgentApplicationService:
         self,
         *,
         agent_run: AgentRun,
-        agent_version: AgentVersion,
+        agent_config: AgentConfig,
         messages: list[ChatMessageContract],
         selected_tools: list[AgentToolRefContract] | None = None) -> ChatCompletionRequestContract:
         function_calling_enabled = self._read_bool(
-            agent_version.model_config_json,
+            agent_config.model_config_json,
             "function_calling_enabled",
             default=False) or self._read_bool(
-            agent_version.model_config_json,
+            agent_config.model_config_json,
             "tool_calling_enabled",
             default=False)
         return ChatCompletionRequestContract(
-            model=self._read_optional_string(agent_version.model_config_json, "model"),
+            model=self._read_optional_string(agent_config.model_config_json, "model"),
             temperature=self._read_optional_float(
-                agent_version.model_config_json,
+                agent_config.model_config_json,
                 "temperature"),
             max_tokens=self._read_optional_int(
-                agent_version.model_config_json,
+                agent_config.model_config_json,
                 "max_tokens"),
             messages=messages,
             tools_json=(
@@ -1040,7 +1033,7 @@ class AgentApplicationService:
             tool_choice="auto" if function_calling_enabled and selected_tools else None,
             metadata_json={
                 "agent_id": agent_run.agent_id,
-                "agent_version_id": agent_version.id,
+                "agent_config_id": agent_config.id,
                 "agent_run_id": agent_run.id,
             })
 
@@ -1112,11 +1105,11 @@ class AgentApplicationService:
             for keyword in keywords
         )
 
-    def _build_dry_run_output(self, *, agent_run: AgentRun, agent_version: AgentVersion) -> str:
+    def _build_dry_run_output(self, *, agent_run: AgentRun, agent_config: AgentConfig) -> str:
         input_preview = agent_run.input_text or str(agent_run.input_json or {})
         return (
-            f"[dry-run] Agent role={agent_version.role} "
-            f"version={agent_version.version_no} received: {input_preview}"
+            f"[dry-run] Agent role={agent_config.role} "
+            f"received: {input_preview}"
         )
 
     def _read_optional_string(self, payload: dict[str, JSONValue], key: str) -> str | None:
@@ -1141,17 +1134,17 @@ class AgentApplicationService:
         self,
         *,
         agent_run: AgentRun,
-        agent_version: AgentVersion) -> tuple[list[MemorySearchResultContract], dict[str, JSONValue]]:
+        agent_config: AgentConfig) -> tuple[list[MemorySearchResultContract], dict[str, JSONValue]]:
         if self.memory_client is None:
             return [], {"memory_read_enabled": False, "memory_read_reason": "client_missing"}
-        if not self._read_bool(agent_version.memory_policy_json, "enabled", default=True):
+        if not self._read_bool(agent_config.memory_policy_json, "enabled", default=True):
             return [], {"memory_read_enabled": False, "memory_read_reason": "policy_disabled"}
 
         query = agent_run.input_text or str(agent_run.input_json or "")
         if not query:
             return [], {"memory_read_enabled": True, "memory_read_count": 0}
 
-        scope = self._resolve_memory_scope(agent_run=agent_run, agent_version=agent_version)
+        scope = self._resolve_memory_scope(agent_run=agent_run, agent_config=agent_config)
         if scope is None:
             return [], {
                 "memory_read_enabled": True,
@@ -1169,7 +1162,7 @@ class AgentApplicationService:
                     owner_agent_id=agent_run.agent_id,
                     session_id=agent_run.session_id,
                     limit=self._read_int(
-                        agent_version.memory_policy_json,
+                        agent_config.memory_policy_json,
                         "read_top_k",
                         default=8))
             )
@@ -1191,14 +1184,14 @@ class AgentApplicationService:
         self,
         *,
         agent_run: AgentRun,
-        agent_version: AgentVersion,
+        agent_config: AgentConfig,
         output_text: str) -> dict[str, JSONValue]:
         if self.memory_client is None:
             return {"memory_write_enabled": False, "memory_write_reason": "client_missing"}
-        if not self._read_bool(agent_version.memory_policy_json, "write_enabled", default=True):
+        if not self._read_bool(agent_config.memory_policy_json, "write_enabled", default=True):
             return {"memory_write_enabled": False, "memory_write_reason": "policy_disabled"}
 
-        scope = self._resolve_memory_scope(agent_run=agent_run, agent_version=agent_version)
+        scope = self._resolve_memory_scope(agent_run=agent_run, agent_config=agent_config)
         if scope is None:
             return {"memory_write_enabled": True, "memory_write_reason": "scope_unavailable"}
 
@@ -1214,20 +1207,19 @@ class AgentApplicationService:
                         output_text=output_text),
                     content_json={
                         "agent_run_id": agent_run.id,
-                        "agent_version_id": agent_version.id,
+                        "agent_config_id": agent_config.id,
                         "input_text": agent_run.input_text,
                         "output_text": output_text,
                     },
                     metadata_json={
                         "source": "agent-service",
-                        "role": agent_version.role,
-                        "version_no": agent_version.version_no,
+                        "role": agent_config.role,
                     },
                     owner_agent_id=agent_run.agent_id,
                     session_id=agent_run.session_id,
                     source_ref=f"agent_run:{agent_run.id}",
                     importance_score=self._read_nested_int(
-                        agent_version.memory_policy_json,
+                        agent_config.memory_policy_json,
                         "config_json",
                         "write_importance_score",
                         default=50))
@@ -1249,9 +1241,9 @@ class AgentApplicationService:
         self,
         *,
         agent_run: AgentRun,
-        agent_version: AgentVersion) -> tuple[MemoryScopeType, str] | None:
+        agent_config: AgentConfig) -> tuple[MemoryScopeType, str] | None:
         scope_value = self._read_optional_string(
-            agent_version.memory_policy_json,
+            agent_config.memory_policy_json,
             "memory_scope") or "session"
         if scope_value == "global":
             return "global", "global"
@@ -1322,7 +1314,7 @@ def build_agent_application_service(
     redis_client = try_build_redis_client(settings.redis_url)
     return AgentApplicationService(
         agent_repository=AgentDefinitionRepository(db),
-        agent_version_repository=AgentVersionRepository(db),
+        agent_config_repository=AgentConfigRepository(db),
         agent_run_repository=AgentRunRepository(db),
         agent_tool_invocation_repository=AgentToolInvocationRepository(db),
         model_gateway_client=ModelGatewayClient(

+ 0 - 1
services/agent-service/app/bootstrap/settings.py

@@ -4,7 +4,6 @@ from core_shared import ServiceSettings
 class AgentServiceSettings(ServiceSettings):
     service_name: str = "agent-service"
     service_port: int = 8007
-    database_url: str = "sqlite:///./agent_service.db"
     model_gateway_service_url: str = "http://127.0.0.1:8005"
     model_gateway_timeout_seconds: float = 60.0
     memory_service_url: str = "http://127.0.0.1:8008"

+ 2 - 2
services/agent-service/app/db/models/__init__.py

@@ -3,12 +3,12 @@ from core_db import Base
 from .agent_definition import AgentDefinition
 from .agent_run import AgentRun
 from .agent_tool_invocation import AgentToolInvocation
-from .agent_version import AgentVersion
+from .agent_config import AgentConfig
 
 __all__ = [
     "AgentDefinition",
     "AgentRun",
     "AgentToolInvocation",
-    "AgentVersion",
+    "AgentConfig",
     "Base",
 ]

+ 5 - 10
services/agent-service/app/db/models/agent_version.py → services/agent-service/app/db/models/agent_config.py

@@ -1,18 +1,14 @@
-from datetime import datetime
-
-from core_db import AuditMixin, Base, EntityMixin, VersionMixin
+from core_db import AuditMixin, Base, EntityMixin
 from core_shared import JSONValue
-from sqlalchemy import DateTime, Integer, String, Text
-from sqlalchemy.dialects.sqlite import JSON
+from sqlalchemy import String, Text
+from sqlalchemy import JSON
 from sqlalchemy.orm import Mapped, mapped_column
 
 
-class AgentVersion(EntityMixin, AuditMixin, VersionMixin, Base):
-    __tablename__ = "agent_version"
+class AgentConfig(EntityMixin, AuditMixin, Base):
+    __tablename__ = "agent_config"
 
     agent_id: Mapped[str] = mapped_column(String(36), index=True)
-    version_no: Mapped[int] = mapped_column(Integer)
-    status: Mapped[str] = mapped_column(String(32), default="draft", index=True)
     role: Mapped[str] = mapped_column(String(64), default="assistant")
     goal: Mapped[str | None] = mapped_column(Text, nullable=True)
     system_prompt: Mapped[str] = mapped_column(Text)
@@ -20,4 +16,3 @@ class AgentVersion(EntityMixin, AuditMixin, VersionMixin, Base):
     memory_policy_json: Mapped[dict[str, JSONValue]] = mapped_column(JSON, default=dict)
     tool_refs_json: Mapped[list[dict[str, JSONValue]]] = mapped_column(JSON, default=list)
     skill_refs_json: Mapped[list[dict[str, JSONValue]]] = mapped_column(JSON, default=list)
-    published_time: Mapped[datetime | None] = mapped_column(DateTime, nullable=True)

+ 3 - 3
services/agent-service/app/db/models/agent_definition.py

@@ -1,11 +1,11 @@
-from core_db import AuditMixin, Base, EntityMixin, VersionMixin
+from core_db import AuditMixin, Base, EntityMixin
 from core_shared import JSONValue
 from sqlalchemy import String, Text
-from sqlalchemy.dialects.sqlite import JSON
+from sqlalchemy import JSON
 from sqlalchemy.orm import Mapped, mapped_column
 
 
-class AgentDefinition(EntityMixin, AuditMixin, VersionMixin, Base):
+class AgentDefinition(EntityMixin, AuditMixin, Base):
     __tablename__ = "agent_definition"
 
     code: Mapped[str] = mapped_column(String(64), index=True)

+ 4 - 4
services/agent-service/app/db/models/agent_run.py

@@ -1,17 +1,17 @@
 from datetime import datetime
 
-from core_db import AuditMixin, Base, EntityMixin, VersionMixin
+from core_db import AuditMixin, Base, EntityMixin
 from core_shared import JSONValue
 from sqlalchemy import DateTime, String, Text
-from sqlalchemy.dialects.sqlite import JSON
+from sqlalchemy import JSON
 from sqlalchemy.orm import Mapped, mapped_column
 
 
-class AgentRun(EntityMixin, AuditMixin, VersionMixin, Base):
+class AgentRun(EntityMixin, AuditMixin, Base):
     __tablename__ = "agent_run"
 
     agent_id: Mapped[str] = mapped_column(String(36), index=True)
-    agent_version_id: Mapped[str] = mapped_column(String(36), index=True)
+    agent_config_id: Mapped[str] = mapped_column(String(36), index=True)
     session_id: Mapped[str | None] = mapped_column(String(36), nullable=True, index=True)
     status: Mapped[str] = mapped_column(String(32), default="queued", index=True)
     worker_key: Mapped[str | None] = mapped_column(String(128), nullable=True)

+ 4 - 4
services/agent-service/app/db/models/agent_tool_invocation.py

@@ -1,18 +1,18 @@
 from datetime import datetime
 
-from core_db import AuditMixin, Base, EntityMixin, VersionMixin
+from core_db import AuditMixin, Base, EntityMixin
 from core_shared import JSONValue
 from sqlalchemy import DateTime, String, Text
-from sqlalchemy.dialects.sqlite import JSON
+from sqlalchemy import JSON
 from sqlalchemy.orm import Mapped, mapped_column
 
 
-class AgentToolInvocation(EntityMixin, AuditMixin, VersionMixin, Base):
+class AgentToolInvocation(EntityMixin, AuditMixin, Base):
     __tablename__ = "agent_tool_invocation"
 
     agent_run_id: Mapped[str] = mapped_column(String(36), index=True)
     agent_id: Mapped[str] = mapped_column(String(36), index=True)
-    agent_version_id: Mapped[str] = mapped_column(String(36), index=True)
+    agent_config_id: Mapped[str] = mapped_column(String(36), index=True)
     tool_code: Mapped[str | None] = mapped_column(String(128), nullable=True, index=True)
     tool_binding_id: Mapped[str | None] = mapped_column(String(36), nullable=True, index=True)
     status: Mapped[str] = mapped_column(String(32), default="selected", index=True)

+ 55 - 43
services/agent-service/app/domain/repositories.py

@@ -1,16 +1,15 @@
 from datetime import datetime
 
-from sqlalchemy import func, select
+from sqlalchemy import delete, select
 from sqlalchemy.orm import Session
 
 from core_domain import (
     AgentRunStatus,
     AgentStatus,
-    AgentToolInvocationStatus,
-    AgentVersionStatus)
+    AgentToolInvocationStatus)
 from core_shared import JSONValue
 
-from app.db.models import AgentDefinition, AgentRun, AgentToolInvocation, AgentVersion
+from app.db.models import AgentDefinition, AgentRun, AgentToolInvocation, AgentConfig
 
 
 class AgentDefinitionRepository:
@@ -52,6 +51,14 @@ class AgentDefinitionRepository:
         )
         return self.db.scalar(stmt)
 
+    def delete(self, *, agent_id: str) -> AgentDefinition | None:
+        entity = self.get_by_id(agent_id=agent_id)
+        if entity is None:
+            return None
+        self.db.delete(entity)
+        self.db.commit()
+        return entity
+
     def update(
         self,
         *,
@@ -86,7 +93,7 @@ class AgentDefinitionRepository:
         return entity
 
 
-class AgentVersionRepository:
+class AgentConfigRepository:
     def __init__(self, db: Session) -> None:
         self.db = db
 
@@ -94,71 +101,56 @@ class AgentVersionRepository:
         self,
         *,
         agent_id: str,
-        status: AgentVersionStatus,
         role: str,
         goal: str | None,
         system_prompt: str,
         model_config_json: dict[str, JSONValue],
         memory_policy_json: dict[str, JSONValue],
         tool_refs_json: list[dict[str, JSONValue]],
-        skill_refs_json: list[dict[str, JSONValue]]) -> AgentVersion:
-        version_no = self._next_version_no(agent_id)
-        entity = AgentVersion(
+        skill_refs_json: list[dict[str, JSONValue]]) -> AgentConfig:
+        entity = AgentConfig(
             agent_id=agent_id,
-            version_no=version_no,
-            status=status,
             role=role,
             goal=goal,
             system_prompt=system_prompt,
             model_config_json=model_config_json,
             memory_policy_json=memory_policy_json,
             tool_refs_json=tool_refs_json,
-            skill_refs_json=skill_refs_json,
-            published_time=datetime.utcnow() if status == "published" else None)
+            skill_refs_json=skill_refs_json)
         self.db.add(entity)
         self.db.commit()
         self.db.refresh(entity)
         return entity
 
-    def list_by_agent(self, *, agent_id: str) -> list[AgentVersion]:
+    def list_by_agent(self, *, agent_id: str) -> list[AgentConfig]:
         stmt = (
-            select(AgentVersion)
-            .where(AgentVersion.agent_id == agent_id)
-            .order_by(AgentVersion.version_no.desc())
+            select(AgentConfig)
+            .where(AgentConfig.agent_id == agent_id)
+            .order_by(AgentConfig.created_time.desc())
         )
         return list(self.db.scalars(stmt))
 
-    def get_by_id(self, *, agent_version_id: str) -> AgentVersion | None:
-        stmt = (
-            select(AgentVersion)
-            .where(AgentVersion.id == agent_version_id)
-        )
-        return self.db.scalar(stmt)
-
-    def get_latest_published(self, *, agent_id: str) -> AgentVersion | None:
+    def get_by_id(self, *, agent_config_id: str) -> AgentConfig | None:
         stmt = (
-            select(AgentVersion)
-            .where(AgentVersion.agent_id == agent_id)
-            .where(AgentVersion.status == "published")
-            .order_by(AgentVersion.version_no.desc())
-            .limit(1)
+            select(AgentConfig)
+            .where(AgentConfig.id == agent_config_id)
         )
         return self.db.scalar(stmt)
 
-    def get_latest_by_agent(self, *, agent_id: str) -> AgentVersion | None:
+    def get_latest_by_agent(self, *, agent_id: str) -> AgentConfig | None:
         stmt = (
-            select(AgentVersion)
-            .where(AgentVersion.agent_id == agent_id)
-            .order_by(AgentVersion.version_no.desc())
+            select(AgentConfig)
+            .where(AgentConfig.agent_id == agent_id)
+            .order_by(AgentConfig.created_time.desc())
             .limit(1)
         )
         return self.db.scalar(stmt)
 
-    def _next_version_no(self, agent_id: str) -> int:
-        stmt = select(func.max(AgentVersion.version_no)).where(AgentVersion.agent_id == agent_id)
-        current_max = self.db.scalar(stmt)
-        return (current_max or 0) + 1
-
+    def delete_by_agent(self, *, agent_id: str) -> int:
+        result = self.db.execute(
+            delete(AgentConfig).where(AgentConfig.agent_id == agent_id))
+        self.db.commit()
+        return int(result.rowcount or 0)
 
 class AgentRunRepository:
     def __init__(self, db: Session) -> None:
@@ -168,14 +160,14 @@ class AgentRunRepository:
         self,
         *,
         agent_id: str,
-        agent_version_id: str,
+        agent_config_id: str,
         session_id: str | None,
         input_text: str | None,
         input_json: dict[str, JSONValue] | None) -> AgentRun:
         now = datetime.utcnow()
         entity = AgentRun(
             agent_id=agent_id,
-            agent_version_id=agent_version_id,
+            agent_config_id=agent_config_id,
             session_id=session_id,
             input_text=input_text,
             input_json=input_json,
@@ -206,6 +198,12 @@ class AgentRunRepository:
         )
         return self.db.scalar(stmt)
 
+    def delete_by_agent(self, *, agent_id: str) -> int:
+        result = self.db.execute(
+            delete(AgentRun).where(AgentRun.agent_id == agent_id))
+        self.db.commit()
+        return int(result.rowcount or 0)
+
     def claim_next_queued(
         self,
         *,
@@ -295,7 +293,7 @@ class AgentToolInvocationRepository:
         *,
         agent_run_id: str,
         agent_id: str,
-        agent_version_id: str,
+        agent_config_id: str,
         tool_code: str | None,
         tool_binding_id: str | None,
         status: AgentToolInvocationStatus,
@@ -304,7 +302,7 @@ class AgentToolInvocationRepository:
         entity = AgentToolInvocation(
             agent_run_id=agent_run_id,
             agent_id=agent_id,
-            agent_version_id=agent_version_id,
+            agent_config_id=agent_config_id,
             tool_code=tool_code,
             tool_binding_id=tool_binding_id,
             status=status,
@@ -326,6 +324,20 @@ class AgentToolInvocationRepository:
         )
         return list(self.db.scalars(stmt))
 
+    def delete_by_run(self, *, agent_run_id: str) -> int:
+        result = self.db.execute(
+            delete(AgentToolInvocation).where(
+                AgentToolInvocation.agent_run_id == agent_run_id))
+        self.db.commit()
+        return int(result.rowcount or 0)
+
+    def delete_by_agent(self, *, agent_id: str) -> int:
+        result = self.db.execute(
+            delete(AgentToolInvocation).where(
+                AgentToolInvocation.agent_id == agent_id))
+        self.db.commit()
+        return int(result.rowcount or 0)
+
     def update_status(
         self,
         *,

+ 74 - 7
services/agent-service/app/infrastructure/memory_client.py

@@ -20,10 +20,10 @@ class MemoryClient:
         try:
             with httpx.Client(timeout=self.timeout_seconds) as client:
                 response = client.post(
-                    f"{self.base_url}/memories",
-                    json=payload.model_dump(mode="json"))
+                    f"{self.base_url}/memories/create",
+                    json=_create_payload_to_contract(payload))
                 response.raise_for_status()
-                return MemoryItemContract.model_validate(response.json())
+                return MemoryItemContract.model_validate(_memory_dto_to_contract(_unwrap(response.json())))
         except httpx.HTTPError as exc:
             raise MemoryClientError(f"memory-service create request failed: {exc}") from exc
 
@@ -33,12 +33,79 @@ class MemoryClient:
         try:
             with httpx.Client(timeout=self.timeout_seconds) as client:
                 response = client.post(
-                    f"{self.base_url}/memories/search",
-                    json=payload.model_dump(mode="json"))
+                    f"{self.base_url}/memories/search/query",
+                    json=_search_payload_to_contract(payload))
                 response.raise_for_status()
                 return [
-                    MemorySearchResultContract.model_validate(item)
-                    for item in response.json()
+                    MemorySearchResultContract.model_validate({
+                        "item": _memory_dto_to_contract(item["item"]),
+                        "score": item["score"],
+                        "score_json": item.get("scoreDetails", {}),
+                    })
+                    for item in _unwrap(response.json())
                 ]
         except httpx.HTTPError as exc:
             raise MemoryClientError(f"memory-service search request failed: {exc}") from exc
+
+
+def _unwrap(payload: dict) -> object:
+    if not payload.get("success", False):
+        message = payload.get("error", {}).get("message", "memory-service request failed")
+        raise MemoryClientError(str(message))
+    return payload.get("data")
+
+
+def _create_payload_to_contract(payload: MemoryCreateContract) -> dict:
+    data = payload.model_dump(mode="json")
+    return {
+        "scopeType": data["scope_type"],
+        "scopeId": data["scope_id"],
+        "memoryType": data.get("memory_type", "fact"),
+        "contentText": data["content_text"],
+        "content": data.get("content_json"),
+        "metadata": data.get("metadata_json", {}),
+        "ownerAgentId": data.get("owner_agent_id"),
+        "userId": data.get("user_id"),
+        "sessionId": data.get("session_id"),
+        "sourceRef": data.get("source_ref"),
+        "importanceScore": data.get("importance_score", 0),
+        "expiresTime": data.get("expires_time"),
+    }
+
+
+def _search_payload_to_contract(payload: MemorySearchRequestContract) -> dict:
+    data = payload.model_dump(mode="json")
+    return {
+        "query": data["query"],
+        "scopeType": data.get("scope_type"),
+        "scopeId": data.get("scope_id"),
+        "ownerAgentId": data.get("owner_agent_id"),
+        "userId": data.get("user_id"),
+        "sessionId": data.get("session_id"),
+        "limit": data.get("limit", 8),
+    }
+
+
+def _memory_dto_to_contract(item: object) -> dict:
+    if not isinstance(item, dict):
+        raise MemoryClientError("invalid memory-service response")
+    return {
+        "id": item["id"],
+        "scope_type": item["scopeType"],
+        "scope_id": item["scopeId"],
+        "memory_type": item["memoryType"],
+        "content_text": item["contentText"],
+        "content_json": item.get("content"),
+        "metadata_json": item.get("metadata", {}),
+        "embedding_model": item.get("embeddingModel"),
+        "embedding_json": item.get("embedding"),
+        "owner_agent_id": item.get("ownerAgentId"),
+        "user_id": item.get("userId"),
+        "session_id": item.get("sessionId"),
+        "source_ref": item.get("sourceRef"),
+        "importance_score": item.get("importanceScore", 0),
+        "status": item["status"],
+        "last_accessed_time": item.get("lastAccessedTime"),
+        "expires_time": item.get("expiresTime"),
+        "created_time": item["createdTime"],
+    }

+ 0 - 3
services/agent-service/app/infrastructure/skill_client.py

@@ -29,15 +29,12 @@ class SkillServiceClient:
         self,
         *,
         skill_id: str,
-        skill_version_id: str | None,
         installation_id: str | None,
         input_json: dict[str, JSONValue]) -> SkillRunContract:
         payload: dict[str, JSONValue] = {
             "skill_id": skill_id,
             "input_json": input_json,
         }
-        if skill_version_id is not None:
-            payload["skill_version_id"] = skill_version_id
         if installation_id is not None:
             payload["installation_id"] = installation_id
 

+ 2 - 2
services/agent-service/app/infrastructure/tool_client.py

@@ -30,7 +30,7 @@ class ToolServiceClient:
         detail: ToolBindingDetailContract,
         input_json: dict[str, JSONValue],
         config_json: dict[str, JSONValue]) -> tuple[str | None, dict[str, JSONValue]]:
-        invoke_config_json = detail.tool_version.invoke_config_json or {}
+        invoke_config_json = detail.connection.invoke_config_json or {}
         binding_config_json = detail.binding.config_json or {}
 
         url = _read_string(config_json, "url") or _read_string(invoke_config_json, "url")
@@ -75,7 +75,7 @@ class ToolServiceClient:
         return response_text, {
             "tool_binding_id": detail.binding.id,
             "tool_code": detail.tool_definition.code,
-            "tool_version_id": detail.tool_version.id,
+            "tool_connection_id": detail.connection.id,
             "tool_name": detail.tool_definition.name,
             "request_url": resolved_url,
             "request_method": method,

+ 41 - 15
services/agent-service/app/schemas/agent.py

@@ -11,14 +11,12 @@ from core_domain import (
     AgentStatus,
     AgentToolInvocationContract,
     AgentToolRefContract,
-    AgentVersionContract,
-    AgentVersionStatus,
 )
 from core_shared import JSONValue
 from pydantic import BaseModel, ConfigDict, Field
 
 if TYPE_CHECKING:
-    from app.db.models import AgentDefinition, AgentRun, AgentToolInvocation, AgentVersion
+    from app.db.models import AgentDefinition, AgentRun, AgentToolInvocation, AgentConfig
 
 
 class AgentCreateRequest(BaseModel):
@@ -37,10 +35,32 @@ class AgentUpdateRequest(BaseModel):
     metadata_json: dict[str, JSONValue] | None = None
 
 
+class AgentListRequest(BaseModel):
+    pass
+
+
+class AgentDetailRequest(BaseModel):
+    agent_id: str
+
+
+class AgentDeleteRequest(BaseModel):
+    agent_id: str
+
+
+class DeleteData(BaseModel):
+    deleted: bool
+    agent_id: str | None = None
+    agent_run_id: str | None = None
+
+
 class AgentStatusUpdateRequest(BaseModel):
     status: AgentStatus
 
 
+class AgentStatusPostRequest(AgentStatusUpdateRequest):
+    agent_id: str
+
+
 class AgentResponse(AgentDefinitionContract):
     @classmethod
     def from_entity(cls, entity: "AgentDefinition") -> "AgentResponse":
@@ -65,16 +85,6 @@ class AgentConfigCreateRequest(BaseModel):
     skill_refs: list[AgentSkillRefContract] = Field(default_factory=list)
 
 
-class AgentVersionCreateRequest(AgentConfigCreateRequest):
-    status: AgentVersionStatus = "draft"
-
-
-class AgentVersionResponse(AgentVersionContract):
-    @classmethod
-    def from_entity(cls, entity: "AgentVersion") -> "AgentVersionResponse":
-        return cls.model_validate(entity, from_attributes=True)
-
-
 class AgentConfigResponse(BaseModel):
     id: str
     agent_id: str
@@ -88,7 +98,7 @@ class AgentConfigResponse(BaseModel):
     created_time: datetime
 
     @classmethod
-    def from_entity(cls, entity: "AgentVersion") -> "AgentConfigResponse":
+    def from_entity(cls, entity: "AgentConfig") -> "AgentConfigResponse":
         return cls(
             id=entity.id,
             agent_id=entity.agent_id,
@@ -104,7 +114,6 @@ class AgentConfigResponse(BaseModel):
 
 class AgentRunCreateRequest(BaseModel):
     agent_id: str
-    agent_version_id: str | None = None
     agent_config_id: str | None = None
     session_id: str | None = None
     input_text: str | None = None
@@ -115,6 +124,15 @@ class AgentRunDetailRequest(BaseModel):
     agent_run_id: str
 
 
+class AgentRunListRequest(BaseModel):
+    agent_id: str | None = None
+    session_id: str | None = None
+
+
+class AgentToolInvocationListRequest(BaseModel):
+    agent_run_id: str
+
+
 class AgentRunStatusUpdateRequest(BaseModel):
     status: AgentRunStatus
     worker_key: str | None = None
@@ -124,11 +142,19 @@ class AgentRunStatusUpdateRequest(BaseModel):
     error_message: str | None = None
 
 
+class AgentRunStatusPostRequest(AgentRunStatusUpdateRequest):
+    agent_run_id: str
+
+
 class AgentRunExecuteRequest(BaseModel):
     worker_key: str | None = None
     dry_run: bool = False
 
 
+class AgentRunExecutePostRequest(AgentRunExecuteRequest):
+    agent_run_id: str
+
+
 class AgentWorkerExecuteNextRequest(BaseModel):
     worker_key: str
     lease_seconds: int | None = Field(default=None, gt=0)

+ 1 - 2
services/api-gateway/alembic.ini

@@ -1,7 +1,7 @@
 [alembic]
 script_location = alembic
 prepend_sys_path = .
-sqlalchemy.url = sqlite:///./api_gateway.db
+sqlalchemy.url = postgresql+psycopg://admin:hFOvG5UBeK5KIGhz5cQH@git.newpoint.work:5432/vectordb
 
 [loggers]
 keys = root,sqlalchemy,alembic
@@ -34,4 +34,3 @@ formatter = generic
 
 [formatter_generic]
 format = %(levelname)-5.5s [%(name)s] %(message)s
-

+ 15 - 2
services/api-gateway/alembic/env.py

@@ -1,10 +1,16 @@
+import os
 from logging.config import fileConfig
 
 from alembic import context
 from app.db.models import Base
 from sqlalchemy import engine_from_config, pool
 
+SERVICE_VERSION_TABLE = "api_gateway_alembic_version"
+
 config = context.config
+database_url = os.getenv("AGENT_PLATFORM_DATABASE_URL")
+if database_url:
+    config.set_main_option("sqlalchemy.url", database_url.replace("%", "%%"))
 
 if config.config_file_name is not None:
     fileConfig(config.config_file_name)
@@ -14,7 +20,11 @@ target_metadata = Base.metadata
 
 def run_migrations_offline() -> None:
     url = config.get_main_option("sqlalchemy.url")
-    context.configure(url=url, target_metadata=target_metadata, literal_binds=True)
+    context.configure(
+        url=url,
+        target_metadata=target_metadata,
+        literal_binds=True,
+        version_table=SERVICE_VERSION_TABLE)
 
     with context.begin_transaction():
         context.run_migrations()
@@ -27,7 +37,10 @@ def run_migrations_online() -> None:
         poolclass=pool.NullPool)
 
     with connectable.connect() as connection:
-        context.configure(connection=connection, target_metadata=target_metadata)
+        context.configure(
+            connection=connection,
+            target_metadata=target_metadata,
+            version_table=SERVICE_VERSION_TABLE)
 
         with context.begin_transaction():
             context.run_migrations()

+ 22 - 0
services/api-gateway/alembic/versions/20260429_9001_remove_version_columns.py

@@ -0,0 +1,22 @@
+"""Remove business version schema artifacts.
+
+Revision ID: 20260429_9001_gateway
+Revises: 20260423_0002
+Create Date: 2026-04-29 00:00:00.000000
+"""
+
+from alembic import op
+
+revision: str = "20260429_9001_gateway"
+down_revision: str | None = "20260423_0002"
+branch_labels = None
+depends_on = None
+
+
+def upgrade() -> None:
+    op.execute("DO $$\nDECLARE\n    table_record record;\nBEGIN\n    FOR table_record IN\n        SELECT table_name\n        FROM information_schema.columns\n        WHERE table_schema = current_schema()\n          AND column_name = 'version'\n    LOOP\n        EXECUTE format('ALTER TABLE %I DROP COLUMN IF EXISTS version', table_record.table_name);\n    END LOOP;\nEND $$;")
+
+
+def downgrade() -> None:
+    # Business version tables and columns were intentionally removed.
+    pass

+ 24 - 0
services/api-gateway/app/api/routes.py

@@ -14,7 +14,9 @@ from app.infrastructure.proxy import ProxyServiceName, ProxyTarget, ServiceProxy
 from app.schemas.gateway import (
     ApiKeyCreateRequest,
     ApiKeyCreateResponse,
+    ApiKeyListRequest,
     ApiKeyResponse,
+    ApiKeyStatusPostRequest,
     ApiKeyStatusUpdateRequest,
     GatewayAuditServiceStats,
     GatewayAuditStatsResponse,
@@ -69,6 +71,16 @@ def list_api_keys(
     ]
 
 
+@router.post("/gateway/api-keys/list", response_model=list[ApiKeyResponse])
+def list_api_keys_post(
+    payload: ApiKeyListRequest,
+    db: DbSession) -> list[ApiKeyResponse]:
+    return [
+        ApiKeyResponse.from_entity(item)
+        for item in ApiKeyRepository(db).list_all()
+    ]
+
+
 @router.patch("/gateway/api-keys/{api_key_id}/status", response_model=ApiKeyResponse)
 def update_api_key_status(
     api_key_id: str,
@@ -82,6 +94,18 @@ def update_api_key_status(
     return ApiKeyResponse.from_entity(entity)
 
 
+@router.post("/gateway/api-keys/status", response_model=ApiKeyResponse)
+def update_api_key_status_post(
+    payload: ApiKeyStatusPostRequest,
+    db: DbSession) -> ApiKeyResponse:
+    entity = ApiKeyRepository(db).update_status(
+        api_key_id=payload.api_key_id,
+        status=payload.status)
+    if entity is None:
+        raise HTTPException(status_code=404, detail=f"api key not found: {payload.api_key_id}")
+    return ApiKeyResponse.from_entity(entity)
+
+
 @router.get("/gateway/audits", response_model=list[GatewayRequestAuditResponse])
 def list_gateway_audits(
     db: DbSession,

+ 0 - 1
services/api-gateway/app/bootstrap/settings.py

@@ -4,7 +4,6 @@ from core_shared import ServiceSettings
 class ApiGatewaySettings(ServiceSettings):
     service_name: str = "api-gateway"
     service_port: int = 8000
-    database_url: str = "sqlite:///./api_gateway.db"
     workflow_service_url: str = "http://127.0.0.1:8002"
     session_service_url: str = "http://127.0.0.1:8001"
     runtime_service_url: str = "http://127.0.0.1:8003"

+ 2 - 2
services/api-gateway/app/db/models/api_key.py

@@ -1,11 +1,11 @@
 from datetime import datetime
 
-from core_db import AuditMixin, Base, EntityMixin, VersionMixin
+from core_db import AuditMixin, Base, EntityMixin
 from sqlalchemy import DateTime, String, Text
 from sqlalchemy.orm import Mapped, mapped_column
 
 
-class ApiKey(EntityMixin, AuditMixin, VersionMixin, Base):
+class ApiKey(EntityMixin, AuditMixin, Base):
     __tablename__ = "api_key"
 
     name: Mapped[str] = mapped_column(String(128))

+ 2 - 2
services/api-gateway/app/db/models/gateway_request_audit.py

@@ -1,9 +1,9 @@
-from core_db import AuditMixin, Base, EntityMixin, VersionMixin
+from core_db import AuditMixin, Base, EntityMixin
 from sqlalchemy import Integer, String, Text
 from sqlalchemy.orm import Mapped, mapped_column
 
 
-class GatewayRequestAudit(EntityMixin, AuditMixin, VersionMixin, Base):
+class GatewayRequestAudit(EntityMixin, AuditMixin, Base):
     __tablename__ = "gateway_request_audit"
 
     request_id: Mapped[str] = mapped_column(String(64), index=True)

+ 8 - 0
services/api-gateway/app/schemas/gateway.py

@@ -60,6 +60,10 @@ class ApiKeyCreateRequest(BaseModel):
     expires_time: datetime | None = None
 
 
+class ApiKeyListRequest(BaseModel):
+    pass
+
+
 class ApiKeyCreateResponse(BaseModel):
     id: str
     name: str
@@ -91,3 +95,7 @@ ApiKeyStatus = Literal["active", "disabled", "revoked"]
 
 class ApiKeyStatusUpdateRequest(BaseModel):
     status: ApiKeyStatus
+
+
+class ApiKeyStatusPostRequest(ApiKeyStatusUpdateRequest):
+    api_key_id: str

+ 1 - 1
services/auth-service/alembic.ini

@@ -1,7 +1,7 @@
 [alembic]
 script_location = alembic
 prepend_sys_path = .
-sqlalchemy.url = sqlite:///./auth_service.db
+sqlalchemy.url = postgresql+psycopg://admin:hFOvG5UBeK5KIGhz5cQH@git.newpoint.work:5432/vectordb
 
 [loggers]
 keys = root,sqlalchemy,alembic

+ 14 - 3
services/auth-service/alembic/env.py

@@ -1,3 +1,4 @@
+import os
 from logging.config import fileConfig
 
 from alembic import context
@@ -5,8 +6,11 @@ from app.bootstrap.settings import AuthServiceSettings
 from app.db.models import Base
 from sqlalchemy import engine_from_config, pool
 
+SERVICE_VERSION_TABLE = "auth_alembic_version"
+
 config = context.config
-config.set_main_option("sqlalchemy.url", AuthServiceSettings().database_url)
+database_url = os.getenv("AGENT_PLATFORM_DATABASE_URL") or AuthServiceSettings().database_url
+config.set_main_option("sqlalchemy.url", database_url.replace("%", "%%"))
 
 if config.config_file_name is not None:
     fileConfig(config.config_file_name)
@@ -16,7 +20,11 @@ target_metadata = Base.metadata
 
 def run_migrations_offline() -> None:
     url = config.get_main_option("sqlalchemy.url")
-    context.configure(url=url, target_metadata=target_metadata, literal_binds=True)
+    context.configure(
+        url=url,
+        target_metadata=target_metadata,
+        literal_binds=True,
+        version_table=SERVICE_VERSION_TABLE)
     with context.begin_transaction():
         context.run_migrations()
 
@@ -27,7 +35,10 @@ def run_migrations_online() -> None:
         prefix="sqlalchemy.",
         poolclass=pool.NullPool)
     with connectable.connect() as connection:
-        context.configure(connection=connection, target_metadata=target_metadata)
+        context.configure(
+            connection=connection,
+            target_metadata=target_metadata,
+            version_table=SERVICE_VERSION_TABLE)
         with context.begin_transaction():
             context.run_migrations()
 

+ 78 - 56
services/auth-service/alembic/versions/20260425_0001_init_auth_models.py

@@ -17,63 +17,72 @@ depends_on: Sequence[str] | None = None
 
 
 def upgrade() -> None:
-    op.create_table(
-        "auth_user",
-        sa.Column("username", sa.String(length=128), nullable=False),
-        sa.Column("display_name", sa.String(length=128), nullable=True),
-        sa.Column("email", sa.String(length=256), nullable=True),
-        sa.Column("status", sa.String(length=32), nullable=False),
-        sa.Column("metadata_json", sa.JSON(), nullable=False),
-        sa.Column("last_login_time", sa.DateTime(), nullable=True),
-        sa.Column("id", sa.String(length=36), nullable=False),
-        sa.Column("created_by", sa.String(length=36), nullable=True),
-        sa.Column("updated_by", sa.String(length=36), nullable=True),
-        sa.Column("created_time", sa.DateTime(), nullable=False),
-        sa.Column("updated_time", sa.DateTime(), nullable=False),
-        sa.Column("deleted_time", sa.DateTime(), nullable=True),
-        sa.Column("version", sa.Integer(), nullable=False),
-        sa.PrimaryKeyConstraint("id"))
-    op.create_table(
-        "auth_role",
-        sa.Column("code", sa.String(length=128), nullable=False),
-        sa.Column("name", sa.String(length=128), nullable=False),
-        sa.Column("description", sa.Text(), nullable=True),
-        sa.Column("status", sa.String(length=32), nullable=False),
-        sa.Column("permissions_json", sa.JSON(), nullable=False),
-        sa.Column("id", sa.String(length=36), nullable=False),
-        sa.Column("created_by", sa.String(length=36), nullable=True),
-        sa.Column("updated_by", sa.String(length=36), nullable=True),
-        sa.Column("created_time", sa.DateTime(), nullable=False),
-        sa.Column("updated_time", sa.DateTime(), nullable=False),
-        sa.Column("deleted_time", sa.DateTime(), nullable=True),
-        sa.Column("version", sa.Integer(), nullable=False),
-        sa.PrimaryKeyConstraint("id"))
-    op.create_table(
-        "auth_role_assignment",
-        sa.Column("user_id", sa.String(length=36), nullable=False),
-        sa.Column("role_id", sa.String(length=36), nullable=False),
-        sa.Column("status", sa.String(length=32), nullable=False),
-        sa.Column("scope_type", sa.String(length=64), nullable=True),
-        sa.Column("scope_id", sa.String(length=64), nullable=True),
-        sa.Column("expires_time", sa.DateTime(), nullable=True),
-        sa.Column("id", sa.String(length=36), nullable=False),
-        sa.Column("created_by", sa.String(length=36), nullable=True),
-        sa.Column("updated_by", sa.String(length=36), nullable=True),
-        sa.Column("created_time", sa.DateTime(), nullable=False),
-        sa.Column("updated_time", sa.DateTime(), nullable=False),
-        sa.Column("deleted_time", sa.DateTime(), nullable=True),
-        sa.Column("version", sa.Integer(), nullable=False),
-        sa.PrimaryKeyConstraint("id"))
+    if not _has_table("auth_user"):
+        op.create_table(
+            "auth_user",
+            sa.Column("username", sa.String(length=128), nullable=False),
+            sa.Column("display_name", sa.String(length=128), nullable=True),
+            sa.Column("email", sa.String(length=256), nullable=True),
+            sa.Column("status", sa.String(length=32), nullable=False),
+            sa.Column("metadata_json", sa.JSON(), nullable=False),
+            sa.Column("last_login_time", sa.DateTime(), nullable=True),
+            sa.Column("id", sa.String(length=36), nullable=False),
+            sa.Column("created_by", sa.String(length=36), nullable=True),
+            sa.Column("updated_by", sa.String(length=36), nullable=True),
+            sa.Column("created_time", sa.DateTime(), nullable=False),
+            sa.Column("updated_time", sa.DateTime(), nullable=False),
+            sa.Column("deleted_time", sa.DateTime(), nullable=True),
+            sa.Column("version", sa.Integer(), nullable=False),
+            sa.PrimaryKeyConstraint("id"))
+    if not _has_table("auth_role"):
+        op.create_table(
+            "auth_role",
+            sa.Column("code", sa.String(length=128), nullable=False),
+            sa.Column("name", sa.String(length=128), nullable=False),
+            sa.Column("description", sa.Text(), nullable=True),
+            sa.Column("status", sa.String(length=32), nullable=False),
+            sa.Column("permissions_json", sa.JSON(), nullable=False),
+            sa.Column("id", sa.String(length=36), nullable=False),
+            sa.Column("created_by", sa.String(length=36), nullable=True),
+            sa.Column("updated_by", sa.String(length=36), nullable=True),
+            sa.Column("created_time", sa.DateTime(), nullable=False),
+            sa.Column("updated_time", sa.DateTime(), nullable=False),
+            sa.Column("deleted_time", sa.DateTime(), nullable=True),
+            sa.Column("version", sa.Integer(), nullable=False),
+            sa.PrimaryKeyConstraint("id"))
+    if not _has_table("auth_role_assignment"):
+        op.create_table(
+            "auth_role_assignment",
+            sa.Column("user_id", sa.String(length=36), nullable=False),
+            sa.Column("role_id", sa.String(length=36), nullable=False),
+            sa.Column("status", sa.String(length=32), nullable=False),
+            sa.Column("scope_type", sa.String(length=64), nullable=True),
+            sa.Column("scope_id", sa.String(length=64), nullable=True),
+            sa.Column("expires_time", sa.DateTime(), nullable=True),
+            sa.Column("id", sa.String(length=36), nullable=False),
+            sa.Column("created_by", sa.String(length=36), nullable=True),
+            sa.Column("updated_by", sa.String(length=36), nullable=True),
+            sa.Column("created_time", sa.DateTime(), nullable=False),
+            sa.Column("updated_time", sa.DateTime(), nullable=False),
+            sa.Column("deleted_time", sa.DateTime(), nullable=True),
+            sa.Column("version", sa.Integer(), nullable=False),
+            sa.PrimaryKeyConstraint("id"))
     for table_name in ("auth_user", "auth_role", "auth_role_assignment"):
-        op.create_index(f"ix_{table_name}_status", table_name, ["status"])
-    op.create_index("ix_auth_user_username", "auth_user", ["username"])
-    op.create_index("ix_auth_user_email", "auth_user", ["email"])
-    op.create_index("ix_auth_role_code", "auth_role", ["code"])
-    op.create_index("ix_auth_role_assignment_user_id", "auth_role_assignment", ["user_id"])
-    op.create_index("ix_auth_role_assignment_role_id", "auth_role_assignment", ["role_id"])
-    op.create_index("ix_auth_role_assignment_scope_type", "auth_role_assignment", ["scope_type"])
-    op.create_index("ix_auth_role_assignment_scope_id", "auth_role_assignment", ["scope_id"])
-    op.create_index(
+        _create_index_if_missing(f"ix_{table_name}_status", table_name, ["status"])
+    _create_index_if_missing("ix_auth_user_username", "auth_user", ["username"])
+    _create_index_if_missing("ix_auth_user_email", "auth_user", ["email"])
+    _create_index_if_missing("ix_auth_role_code", "auth_role", ["code"])
+    _create_index_if_missing("ix_auth_role_assignment_user_id", "auth_role_assignment", ["user_id"])
+    _create_index_if_missing("ix_auth_role_assignment_role_id", "auth_role_assignment", ["role_id"])
+    _create_index_if_missing(
+        "ix_auth_role_assignment_scope_type",
+        "auth_role_assignment",
+        ["scope_type"])
+    _create_index_if_missing(
+        "ix_auth_role_assignment_scope_id",
+        "auth_role_assignment",
+        ["scope_id"])
+    _create_index_if_missing(
         "ix_auth_role_assignment_expires_time",
         "auth_role_assignment",
         ["expires_time"])
@@ -83,3 +92,16 @@ def downgrade() -> None:
     op.drop_table("auth_role_assignment")
     op.drop_table("auth_role")
     op.drop_table("auth_user")
+
+
+def _has_table(table_name: str) -> bool:
+    return sa.inspect(op.get_bind()).has_table(table_name)
+
+
+def _create_index_if_missing(index_name: str, table_name: str, columns: list[str]) -> None:
+    existing_index_names = {
+        index["name"]
+        for index in sa.inspect(op.get_bind()).get_indexes(table_name)
+    }
+    if index_name not in existing_index_names:
+        op.create_index(index_name, table_name, columns)

+ 10 - 2
services/auth-service/alembic/versions/20260427_0002_add_user_password_hash.py

@@ -17,12 +17,20 @@ depends_on: Sequence[str] | None = None
 
 
 def upgrade() -> None:
+    if _has_column("auth_user", "password_hash"):
+        return None
     op.add_column(
         "auth_user",
         sa.Column("password_hash", sa.String(length=512), nullable=False, server_default=""))
-    if op.get_bind().dialect.name != "sqlite":
-        op.alter_column("auth_user", "password_hash", server_default=None)
+    op.alter_column("auth_user", "password_hash", server_default=None)
 
 
 def downgrade() -> None:
     op.drop_column("auth_user", "password_hash")
+
+
+def _has_column(table_name: str, column_name: str) -> bool:
+    return any(
+        column["name"] == column_name
+        for column in sa.inspect(op.get_bind()).get_columns(table_name)
+    )

+ 0 - 3
services/auth-service/alembic/versions/20260427_0003_remove_auth_partition_columns.py

@@ -8,9 +8,6 @@ Create Date: 2026-04-27 23:45:00
 
 from collections.abc import Sequence
 
-import sqlalchemy as sa
-from alembic import op
-
 revision: str = "20260427_0003"
 down_revision: str | None = "20260427_0002"
 branch_labels: Sequence[str] | None = None

+ 64 - 39
services/auth-service/alembic/versions/20260428_0004_add_identity_contract_tables.py

@@ -17,65 +17,90 @@ depends_on: Sequence[str] | None = None
 
 
 def upgrade() -> None:
-    op.create_table(
-        "auth_role_permission_binding",
-        sa.Column("role_id", sa.String(length=36), nullable=False),
-        sa.Column("permission", sa.String(length=256), nullable=False),
-        sa.Column("scope_type", sa.String(length=64), nullable=True),
-        sa.Column("scope_id", sa.String(length=64), nullable=True),
-        sa.Column("id", sa.String(length=36), nullable=False),
-        sa.Column("created_by", sa.String(length=36), nullable=True),
-        sa.Column("updated_by", sa.String(length=36), nullable=True),
-        sa.Column("created_time", sa.DateTime(), nullable=False),
-        sa.Column("updated_time", sa.DateTime(), nullable=False),
-        sa.Column("deleted_time", sa.DateTime(), nullable=True),
-        sa.Column("version", sa.Integer(), nullable=False),
-        sa.PrimaryKeyConstraint("id"),
-    )
-    op.create_index(
+    if not _has_table("auth_role_permission_binding"):
+        op.create_table(
+            "auth_role_permission_binding",
+            sa.Column("role_id", sa.String(length=36), nullable=False),
+            sa.Column("permission", sa.String(length=256), nullable=False),
+            sa.Column("scope_type", sa.String(length=64), nullable=True),
+            sa.Column("scope_id", sa.String(length=64), nullable=True),
+            sa.Column("id", sa.String(length=36), nullable=False),
+            sa.Column("created_by", sa.String(length=36), nullable=True),
+            sa.Column("updated_by", sa.String(length=36), nullable=True),
+            sa.Column("created_time", sa.DateTime(), nullable=False),
+            sa.Column("updated_time", sa.DateTime(), nullable=False),
+            sa.Column("deleted_time", sa.DateTime(), nullable=True),
+            sa.Column("version", sa.Integer(), nullable=False),
+            sa.PrimaryKeyConstraint("id"),
+        )
+    _create_index_if_missing(
         "ix_auth_role_permission_binding_role_id",
         "auth_role_permission_binding",
         ["role_id"],
     )
-    op.create_index(
+    _create_index_if_missing(
         "ix_auth_role_permission_binding_permission",
         "auth_role_permission_binding",
         ["permission"],
     )
-    op.create_index(
+    _create_index_if_missing(
         "ix_auth_role_permission_binding_scope_type",
         "auth_role_permission_binding",
         ["scope_type"],
     )
-    op.create_index(
+    _create_index_if_missing(
         "ix_auth_role_permission_binding_scope_id",
         "auth_role_permission_binding",
         ["scope_id"],
     )
 
-    op.create_table(
+    if not _has_table("auth_api_key"):
+        op.create_table(
+            "auth_api_key",
+            sa.Column("name", sa.String(length=128), nullable=False),
+            sa.Column("key_prefix", sa.String(length=16), nullable=False),
+            sa.Column("key_hash", sa.String(length=128), nullable=False),
+            sa.Column("scopes", sa.Text(), nullable=True),
+            sa.Column("expires_time", sa.DateTime(), nullable=True),
+            sa.Column("last_used_time", sa.DateTime(), nullable=True),
+            sa.Column("revoked_time", sa.DateTime(), nullable=True),
+            sa.Column("id", sa.String(length=36), nullable=False),
+            sa.Column("created_by", sa.String(length=36), nullable=True),
+            sa.Column("updated_by", sa.String(length=36), nullable=True),
+            sa.Column("created_time", sa.DateTime(), nullable=False),
+            sa.Column("updated_time", sa.DateTime(), nullable=False),
+            sa.Column("deleted_time", sa.DateTime(), nullable=True),
+            sa.Column("version", sa.Integer(), nullable=False),
+            sa.PrimaryKeyConstraint("id"),
+        )
+    _create_index_if_missing("ix_auth_api_key_key_prefix", "auth_api_key", ["key_prefix"])
+    _create_index_if_missing(
+        "ix_auth_api_key_key_hash",
         "auth_api_key",
-        sa.Column("name", sa.String(length=128), nullable=False),
-        sa.Column("key_prefix", sa.String(length=16), nullable=False),
-        sa.Column("key_hash", sa.String(length=128), nullable=False),
-        sa.Column("scopes", sa.Text(), nullable=True),
-        sa.Column("expires_time", sa.DateTime(), nullable=True),
-        sa.Column("last_used_time", sa.DateTime(), nullable=True),
-        sa.Column("revoked_time", sa.DateTime(), nullable=True),
-        sa.Column("id", sa.String(length=36), nullable=False),
-        sa.Column("created_by", sa.String(length=36), nullable=True),
-        sa.Column("updated_by", sa.String(length=36), nullable=True),
-        sa.Column("created_time", sa.DateTime(), nullable=False),
-        sa.Column("updated_time", sa.DateTime(), nullable=False),
-        sa.Column("deleted_time", sa.DateTime(), nullable=True),
-        sa.Column("version", sa.Integer(), nullable=False),
-        sa.PrimaryKeyConstraint("id"),
-    )
-    op.create_index("ix_auth_api_key_key_prefix", "auth_api_key", ["key_prefix"])
-    op.create_index("ix_auth_api_key_key_hash", "auth_api_key", ["key_hash"], unique=True)
-    op.create_index("ix_auth_api_key_revoked_time", "auth_api_key", ["revoked_time"])
+        ["key_hash"],
+        unique=True)
+    _create_index_if_missing("ix_auth_api_key_revoked_time", "auth_api_key", ["revoked_time"])
 
 
 def downgrade() -> None:
     op.drop_table("auth_api_key")
     op.drop_table("auth_role_permission_binding")
+
+
+def _has_table(table_name: str) -> bool:
+    return sa.inspect(op.get_bind()).has_table(table_name)
+
+
+def _create_index_if_missing(
+    index_name: str,
+    table_name: str,
+    columns: list[str],
+    *,
+    unique: bool = False,
+) -> None:
+    existing_index_names = {
+        index["name"]
+        for index in sa.inspect(op.get_bind()).get_indexes(table_name)
+    }
+    if index_name not in existing_index_names:
+        op.create_index(index_name, table_name, columns, unique=unique)

+ 22 - 0
services/auth-service/alembic/versions/20260429_9001_remove_version_columns.py

@@ -0,0 +1,22 @@
+"""Remove business version schema artifacts.
+
+Revision ID: 20260429_9001_auth
+Revises: 20260428_0004
+Create Date: 2026-04-29 00:00:00.000000
+"""
+
+from alembic import op
+
+revision: str = "20260429_9001_auth"
+down_revision: str | None = "20260428_0004"
+branch_labels = None
+depends_on = None
+
+
+def upgrade() -> None:
+    op.execute("DO $$\nDECLARE\n    table_record record;\nBEGIN\n    FOR table_record IN\n        SELECT table_name\n        FROM information_schema.columns\n        WHERE table_schema = current_schema()\n          AND column_name = 'version'\n    LOOP\n        EXECUTE format('ALTER TABLE %I DROP COLUMN IF EXISTS version', table_record.table_name);\n    END LOOP;\nEND $$;")
+
+
+def downgrade() -> None:
+    # Business version tables and columns were intentionally removed.
+    pass

+ 10 - 3
services/auth-service/app/api/identity_routes.py

@@ -2,6 +2,7 @@ from datetime import datetime
 from typing import Annotated, TypeVar
 
 from core_domain import ServiceHealth
+from core_shared import try_build_redis_client
 from fastapi import APIRouter, Depends, Header, HTTPException, Request
 from sqlalchemy import text
 from sqlalchemy.orm import Session
@@ -52,7 +53,9 @@ def get_identity_application_service(request: Request, db: DbSession) -> AuthApp
         assignment_repository=RoleAssignmentRepository(db),
         permission_binding_repository=RolePermissionBindingRepository(db),
         api_key_repository=ApiKeyRepository(db),
-        token_secret=settings.credential_encryption_key)
+        token_secret=settings.credential_encryption_key,
+        redis_client=try_build_redis_client(settings.redis_url),
+        permission_cache_ttl_seconds=settings.permission_cache_ttl_seconds)
 
 
 IdentityServiceDep = Annotated[AuthApplicationService, Depends(get_identity_application_service)]
@@ -98,8 +101,12 @@ def login(
 
 
 @router.post("/auth/logout", response_model=ApiResponse[dict[str, bool]])
-def logout(request: Request) -> ApiResponse[dict[str, bool]]:
-    return ok(request, {"ok": True})
+def logout(
+    request: Request,
+    service: IdentityServiceDep,
+    authorization: AuthorizationHeader = None) -> ApiResponse[dict[str, bool]]:
+    access_token = get_bearer_token(authorization) if authorization else None
+    return ok(request, {"ok": service.logout(access_token=access_token)})
 
 
 @router.post("/auth/tokens/verify", response_model=ApiResponse[TokenVerifyData])

+ 155 - 1
services/auth-service/app/application/services.py

@@ -1,3 +1,5 @@
+import hashlib
+import json
 from dataclasses import dataclass
 from datetime import datetime
 
@@ -46,13 +48,17 @@ class AuthApplicationService:
         assignment_repository: RoleAssignmentRepository,
         permission_binding_repository: RolePermissionBindingRepository,
         api_key_repository: ApiKeyRepository,
-        token_secret: str) -> None:
+        token_secret: str,
+        redis_client: object | None = None,
+        permission_cache_ttl_seconds: int = 60) -> None:
         self.user_repository = user_repository
         self.role_repository = role_repository
         self.assignment_repository = assignment_repository
         self.permission_binding_repository = permission_binding_repository
         self.api_key_repository = api_key_repository
         self.token_secret = token_secret
+        self.redis_client = redis_client
+        self.permission_cache_ttl_seconds = permission_cache_ttl_seconds
 
     def login(self, *, username: str, password: str) -> LoginResult | None:
         user = self.user_repository.get_by_username(username=username)
@@ -71,6 +77,9 @@ class AuthApplicationService:
             user=user)
 
     def verify_token(self, *, access_token: str) -> TokenVerificationResult:
+        if self._is_token_revoked(access_token=access_token):
+            return TokenVerificationResult(active=False, reason="token_revoked")
+
         try:
             token_payload = verify_access_token(access_token, secret=self.token_secret)
         except TokenError as exc:
@@ -87,6 +96,25 @@ class AuthApplicationService:
             username=user.username,
             expires_time=datetime.fromisoformat(expires_time_raw.removesuffix("Z")))
 
+    def logout(self, *, access_token: str | None) -> bool:
+        if not access_token or self.redis_client is None:
+            return True
+        try:
+            token_payload = verify_access_token(access_token, secret=self.token_secret)
+        except TokenError:
+            return True
+
+        expires_time = datetime.fromisoformat(token_payload["expires_time"].removesuffix("Z"))
+        ttl_seconds = max(1, int((expires_time - datetime.utcnow()).total_seconds()))
+        try:
+            self.redis_client.set(
+                self._revoked_token_key(access_token=access_token),
+                "1",
+                ex=ttl_seconds)
+        except Exception:
+            return False
+        return True
+
     def list_users(self) -> list[User]:
         return self.user_repository.list_all()
 
@@ -175,6 +203,34 @@ class AuthApplicationService:
         return self.api_key_repository.revoke(api_key_id=api_key_id)
 
     def check_permission(
+        self,
+        *,
+        user_id: str,
+        permission: str,
+        scope_type: str | None,
+        scope_id: str | None) -> PermissionCheckResult:
+        cached_result = self._read_permission_cache(
+            user_id=user_id,
+            permission=permission,
+            scope_type=scope_type,
+            scope_id=scope_id)
+        if cached_result is not None:
+            return cached_result
+
+        result = self._check_permission_uncached(
+            user_id=user_id,
+            permission=permission,
+            scope_type=scope_type,
+            scope_id=scope_id)
+        self._write_permission_cache(
+            user_id=user_id,
+            permission=permission,
+            scope_type=scope_type,
+            scope_id=scope_id,
+            result=result)
+        return result
+
+    def _check_permission_uncached(
         self,
         *,
         user_id: str,
@@ -216,6 +272,104 @@ class AuthApplicationService:
             reason="matched" if matched_role_ids else "permission_not_found",
             matched_role_ids=matched_role_ids)
 
+    def _is_token_revoked(self, *, access_token: str) -> bool:
+        if self.redis_client is None:
+            return False
+        try:
+            return self.redis_client.exists(self._revoked_token_key(access_token=access_token)) > 0
+        except Exception:
+            return False
+
+    def _revoked_token_key(self, *, access_token: str) -> str:
+        return f"auth:revoked-token:{self._token_digest(access_token)}"
+
+    def _token_digest(self, access_token: str) -> str:
+        return hashlib.sha256(access_token.encode("utf-8")).hexdigest()
+
+    def _read_permission_cache(
+        self,
+        *,
+        user_id: str,
+        permission: str,
+        scope_type: str | None,
+        scope_id: str | None) -> PermissionCheckResult | None:
+        if self.redis_client is None:
+            return None
+        try:
+            raw_value = self.redis_client.get(
+                self._permission_cache_key(
+                    user_id=user_id,
+                    permission=permission,
+                    scope_type=scope_type,
+                    scope_id=scope_id))
+        except Exception:
+            return None
+        if not isinstance(raw_value, (bytes, str)):
+            return None
+        decoded = raw_value.decode("utf-8") if isinstance(raw_value, bytes) else raw_value
+        try:
+            payload = json.loads(decoded)
+        except json.JSONDecodeError:
+            return None
+        if not isinstance(payload, dict):
+            return None
+        matched_role_ids = payload.get("matched_role_ids")
+        if not isinstance(matched_role_ids, list):
+            return None
+        return PermissionCheckResult(
+            allowed=bool(payload.get("allowed")),
+            reason=str(payload.get("reason") or "cached"),
+            matched_role_ids=[
+                item for item in matched_role_ids
+                if isinstance(item, str)
+            ])
+
+    def _write_permission_cache(
+        self,
+        *,
+        user_id: str,
+        permission: str,
+        scope_type: str | None,
+        scope_id: str | None,
+        result: PermissionCheckResult) -> None:
+        if self.redis_client is None or self.permission_cache_ttl_seconds <= 0:
+            return
+        payload = {
+            "allowed": result.allowed,
+            "reason": result.reason,
+            "matched_role_ids": result.matched_role_ids,
+        }
+        try:
+            self.redis_client.set(
+                self._permission_cache_key(
+                    user_id=user_id,
+                    permission=permission,
+                    scope_type=scope_type,
+                    scope_id=scope_id),
+                json.dumps(payload, ensure_ascii=False),
+                ex=self.permission_cache_ttl_seconds)
+        except Exception:
+            return
+
+    def _permission_cache_key(
+        self,
+        *,
+        user_id: str,
+        permission: str,
+        scope_type: str | None,
+        scope_id: str | None) -> str:
+        raw_key = json.dumps(
+            {
+                "user_id": user_id,
+                "permission": permission,
+                "scope_type": scope_type,
+                "scope_id": scope_id,
+            },
+            sort_keys=True,
+            separators=(",", ":"))
+        digest = hashlib.sha256(raw_key.encode("utf-8")).hexdigest()
+        return f"auth:permission-check:{digest}"
+
     def _permission_matches(self, permissions: list[str], requested_permission: str) -> bool:
         if "*" in permissions or requested_permission in permissions:
             return True

+ 1 - 3
services/auth-service/app/bootstrap/settings.py

@@ -4,11 +4,9 @@ from core_shared import ServiceSettings
 class AuthServiceSettings(ServiceSettings):
     service_name: str = "auth-service"
     service_port: int = 8014
-    database_url: str = (
-        "postgresql+psycopg://admin:hFOvG5UBeK5KIGhz5cQH@git.newpoint.work:5432/vectordb"
-    )
     demo_user_bootstrap_enabled: bool = True
     demo_user_username: str = "demo-user"
     demo_user_password: str = "demo-password"
     demo_user_display_name: str = "Demo User"
     demo_user_email: str = "demo@example.com"
+    permission_cache_ttl_seconds: int = 60

+ 2 - 2
services/auth-service/app/db/models/api_key.py

@@ -1,11 +1,11 @@
 from datetime import datetime
 
-from core_db import AuditMixin, Base, EntityMixin, VersionMixin
+from core_db import AuditMixin, Base, EntityMixin
 from sqlalchemy import DateTime, String, Text
 from sqlalchemy.orm import Mapped, mapped_column
 
 
-class ApiKey(EntityMixin, AuditMixin, VersionMixin, Base):
+class ApiKey(EntityMixin, AuditMixin, Base):
     __tablename__ = "auth_api_key"
 
     name: Mapped[str] = mapped_column(String(128))

+ 2 - 2
services/auth-service/app/db/models/role.py

@@ -1,11 +1,11 @@
 from uuid import uuid4
 
-from core_db import AuditMixin, Base, VersionMixin
+from core_db import AuditMixin, Base
 from sqlalchemy import JSON, String, Text
 from sqlalchemy.orm import Mapped, mapped_column
 
 
-class Role(Base, AuditMixin, VersionMixin):
+class Role(Base, AuditMixin):
     __tablename__ = "auth_role"
 
     id: Mapped[str] = mapped_column(String(36), primary_key=True, default=lambda: str(uuid4()))

+ 2 - 2
services/auth-service/app/db/models/role_assignment.py

@@ -1,12 +1,12 @@
 from datetime import datetime
 from uuid import uuid4
 
-from core_db import AuditMixin, Base, VersionMixin
+from core_db import AuditMixin, Base
 from sqlalchemy import DateTime, String
 from sqlalchemy.orm import Mapped, mapped_column
 
 
-class RoleAssignment(Base, AuditMixin, VersionMixin):
+class RoleAssignment(Base, AuditMixin):
     __tablename__ = "auth_role_assignment"
 
     id: Mapped[str] = mapped_column(String(36), primary_key=True, default=lambda: str(uuid4()))

+ 2 - 2
services/auth-service/app/db/models/role_permission_binding.py

@@ -1,11 +1,11 @@
 from uuid import uuid4
 
-from core_db import AuditMixin, Base, VersionMixin
+from core_db import AuditMixin, Base
 from sqlalchemy import String
 from sqlalchemy.orm import Mapped, mapped_column
 
 
-class RolePermissionBinding(Base, AuditMixin, VersionMixin):
+class RolePermissionBinding(Base, AuditMixin):
     __tablename__ = "auth_role_permission_binding"
 
     id: Mapped[str] = mapped_column(String(36), primary_key=True, default=lambda: str(uuid4()))

+ 2 - 2
services/auth-service/app/db/models/user.py

@@ -1,13 +1,13 @@
 from datetime import datetime
 from uuid import uuid4
 
-from core_db import AuditMixin, Base, VersionMixin
+from core_db import AuditMixin, Base
 from core_shared import JSONValue
 from sqlalchemy import JSON, DateTime, String
 from sqlalchemy.orm import Mapped, mapped_column
 
 
-class User(AuditMixin, VersionMixin, Base):
+class User(AuditMixin, Base):
     __tablename__ = "auth_user"
 
     id: Mapped[str] = mapped_column(String(36), primary_key=True, default=lambda: str(uuid4()))

+ 1 - 1
services/event-service/alembic.ini

@@ -1,7 +1,7 @@
 [alembic]
 script_location = alembic
 prepend_sys_path = .
-sqlalchemy.url = sqlite:///./event_service.db
+sqlalchemy.url = postgresql+psycopg://admin:hFOvG5UBeK5KIGhz5cQH@git.newpoint.work:5432/vectordb
 
 [loggers]
 keys = root,sqlalchemy,alembic

+ 15 - 2
services/event-service/alembic/env.py

@@ -1,10 +1,16 @@
+import os
 from logging.config import fileConfig
 
 from alembic import context
 from app.db.models import Base
 from sqlalchemy import engine_from_config, pool
 
+SERVICE_VERSION_TABLE = "event_alembic_version"
+
 config = context.config
+database_url = os.getenv("AGENT_PLATFORM_DATABASE_URL")
+if database_url:
+    config.set_main_option("sqlalchemy.url", database_url.replace("%", "%%"))
 
 if config.config_file_name is not None:
     fileConfig(config.config_file_name)
@@ -14,7 +20,11 @@ target_metadata = Base.metadata
 
 def run_migrations_offline() -> None:
     url = config.get_main_option("sqlalchemy.url")
-    context.configure(url=url, target_metadata=target_metadata, literal_binds=True)
+    context.configure(
+        url=url,
+        target_metadata=target_metadata,
+        literal_binds=True,
+        version_table=SERVICE_VERSION_TABLE)
     with context.begin_transaction():
         context.run_migrations()
 
@@ -25,7 +35,10 @@ def run_migrations_online() -> None:
         prefix="sqlalchemy.",
         poolclass=pool.NullPool)
     with connectable.connect() as connection:
-        context.configure(connection=connection, target_metadata=target_metadata)
+        context.configure(
+            connection=connection,
+            target_metadata=target_metadata,
+            version_table=SERVICE_VERSION_TABLE)
         with context.begin_transaction():
             context.run_migrations()
 

+ 22 - 0
services/event-service/alembic/versions/20260429_9001_remove_version_columns.py

@@ -0,0 +1,22 @@
+"""Remove business version schema artifacts.
+
+Revision ID: 20260429_9001_event
+Revises: 20260425_0001
+Create Date: 2026-04-29 00:00:00.000000
+"""
+
+from alembic import op
+
+revision: str = "20260429_9001_event"
+down_revision: str | None = "20260425_0001"
+branch_labels = None
+depends_on = None
+
+
+def upgrade() -> None:
+    op.execute("DO $$\nDECLARE\n    table_record record;\nBEGIN\n    FOR table_record IN\n        SELECT table_name\n        FROM information_schema.columns\n        WHERE table_schema = current_schema()\n          AND column_name = 'version'\n    LOOP\n        EXECUTE format('ALTER TABLE %I DROP COLUMN IF EXISTS version', table_record.table_name);\n    END LOOP;\nEND $$;")
+
+
+def downgrade() -> None:
+    # Business version tables and columns were intentionally removed.
+    pass

+ 40 - 0
services/event-service/app/api/routes.py

@@ -10,7 +10,9 @@ from app.domain.repositories import EventRecordRepository
 from app.schemas.event import (
     EventBatchPublishRequest,
     EventBatchPublishResponse,
+    EventDeliveryStatusPostRequest,
     EventDeliveryStatusUpdateRequest,
+    EventListRequest,
     EventPublishRequest,
     EventRecordResponse,
     EventStatsResponse,
@@ -70,6 +72,23 @@ def list_events(
     ]
 
 
+@router.post("/list", response_model=list[EventRecordResponse])
+def list_events_post(
+    payload: EventListRequest,
+    service: EventApplicationService = Depends(get_event_application_service)) -> list[EventRecordResponse]:
+    return [
+        EventRecordResponse.from_entity(item)
+        for item in service.list_events(
+            event_type=payload.event_type,
+            source_service=payload.source_service,
+            aggregate_type=payload.aggregate_type,
+            aggregate_id=payload.aggregate_id,
+            correlation_id=payload.correlation_id,
+            status=payload.status,
+            limit=payload.limit)
+    ]
+
+
 @router.post("/claim-pending", response_model=list[EventRecordResponse])
 def claim_pending_events(
     payload: PendingEventClaimRequest,
@@ -93,8 +112,29 @@ def update_delivery_status(
     return EventRecordResponse.from_entity(entity)
 
 
+@router.post("/delivery-status", response_model=EventRecordResponse)
+def update_delivery_status_post(
+    payload: EventDeliveryStatusPostRequest,
+    service: EventApplicationService = Depends(get_event_application_service)) -> EventRecordResponse:
+    entity = service.update_delivery_status(
+        event_record_id=payload.event_record_id,
+        payload=EventDeliveryStatusUpdateRequest(
+            status=payload.status,
+            last_error_message=payload.last_error_message))
+    if entity is None:
+        raise HTTPException(status_code=404, detail=f"event not found: {payload.event_record_id}")
+    return EventRecordResponse.from_entity(entity)
+
+
 @router.get("/stats", response_model=EventStatsResponse)
 def event_stats(
     service: EventApplicationService = Depends(get_event_application_service)) -> EventStatsResponse:
     return EventStatsResponse(
         counts_json=service.build_stats())
+
+
+@router.post("/stats", response_model=EventStatsResponse)
+def event_stats_post(
+    service: EventApplicationService = Depends(get_event_application_service)) -> EventStatsResponse:
+    return EventStatsResponse(
+        counts_json=service.build_stats())

+ 0 - 1
services/event-service/app/bootstrap/settings.py

@@ -4,5 +4,4 @@ from core_shared import ServiceSettings
 class EventServiceSettings(ServiceSettings):
     service_name: str = "event-service"
     service_port: int = 8013
-    database_url: str = "sqlite:///./event_service.db"
     default_claim_limit: int = 100

+ 3 - 3
services/event-service/app/db/models/event_record.py

@@ -1,13 +1,13 @@
 from datetime import datetime
 
-from core_db import AuditMixin, Base, EntityMixin, VersionMixin
+from core_db import AuditMixin, Base, EntityMixin
 from core_shared import JSONValue
 from sqlalchemy import DateTime, Integer, String, Text
-from sqlalchemy.dialects.sqlite import JSON
+from sqlalchemy import JSON
 from sqlalchemy.orm import Mapped, mapped_column
 
 
-class EventRecord(EntityMixin, AuditMixin, VersionMixin, Base):
+class EventRecord(EntityMixin, AuditMixin, Base):
     __tablename__ = "event_record"
 
     event_id: Mapped[str] = mapped_column(String(36), unique=True, index=True)

+ 14 - 0
services/event-service/app/schemas/event.py

@@ -18,11 +18,25 @@ class EventRecordResponse(EventRecordContract):
         return cls.model_validate(entity, from_attributes=True)
 
 
+class EventListRequest(BaseModel):
+    event_type: str | None = None
+    source_service: str | None = None
+    aggregate_type: str | None = None
+    aggregate_id: str | None = None
+    correlation_id: str | None = None
+    status: EventDeliveryStatus | None = None
+    limit: int = Field(default=100, ge=1, le=500)
+
+
 class EventDeliveryStatusUpdateRequest(BaseModel):
     status: EventDeliveryStatus
     last_error_message: str | None = None
 
 
+class EventDeliveryStatusPostRequest(EventDeliveryStatusUpdateRequest):
+    event_record_id: str
+
+
 class PendingEventClaimRequest(BaseModel):
     limit: int = Field(default=100, ge=1, le=500)
 

+ 1 - 1
services/human-service/alembic.ini

@@ -1,7 +1,7 @@
 [alembic]
 script_location = alembic
 prepend_sys_path = .
-sqlalchemy.url = sqlite:///./human_service.db
+sqlalchemy.url = postgresql+psycopg://admin:hFOvG5UBeK5KIGhz5cQH@git.newpoint.work:5432/vectordb
 
 [loggers]
 keys = root,sqlalchemy,alembic

+ 15 - 2
services/human-service/alembic/env.py

@@ -1,10 +1,16 @@
+import os
 from logging.config import fileConfig
 
 from alembic import context
 from app.db.models import Base
 from sqlalchemy import engine_from_config, pool
 
+SERVICE_VERSION_TABLE = "human_alembic_version"
+
 config = context.config
+database_url = os.getenv("AGENT_PLATFORM_DATABASE_URL")
+if database_url:
+    config.set_main_option("sqlalchemy.url", database_url.replace("%", "%%"))
 
 if config.config_file_name is not None:
     fileConfig(config.config_file_name)
@@ -14,7 +20,11 @@ target_metadata = Base.metadata
 
 def run_migrations_offline() -> None:
     url = config.get_main_option("sqlalchemy.url")
-    context.configure(url=url, target_metadata=target_metadata, literal_binds=True)
+    context.configure(
+        url=url,
+        target_metadata=target_metadata,
+        literal_binds=True,
+        version_table=SERVICE_VERSION_TABLE)
     with context.begin_transaction():
         context.run_migrations()
 
@@ -25,7 +35,10 @@ def run_migrations_online() -> None:
         prefix="sqlalchemy.",
         poolclass=pool.NullPool)
     with connectable.connect() as connection:
-        context.configure(connection=connection, target_metadata=target_metadata)
+        context.configure(
+            connection=connection,
+            target_metadata=target_metadata,
+            version_table=SERVICE_VERSION_TABLE)
         with context.begin_transaction():
             context.run_migrations()
 

+ 22 - 0
services/human-service/alembic/versions/20260429_9001_remove_version_columns.py

@@ -0,0 +1,22 @@
+"""Remove business version schema artifacts.
+
+Revision ID: 20260429_9001_human
+Revises: 20260425_0001
+Create Date: 2026-04-29 00:00:00.000000
+"""
+
+from alembic import op
+
+revision: str = "20260429_9001_human"
+down_revision: str | None = "20260425_0001"
+branch_labels = None
+depends_on = None
+
+
+def upgrade() -> None:
+    op.execute("DO $$\nDECLARE\n    table_record record;\nBEGIN\n    FOR table_record IN\n        SELECT table_name\n        FROM information_schema.columns\n        WHERE table_schema = current_schema()\n          AND column_name = 'version'\n    LOOP\n        EXECUTE format('ALTER TABLE %I DROP COLUMN IF EXISTS version', table_record.table_name);\n    END LOOP;\nEND $$;")
+
+
+def downgrade() -> None:
+    # Business version tables and columns were intentionally removed.
+    pass

+ 54 - 0
services/human-service/app/api/routes.py

@@ -8,8 +8,12 @@ from app.db.session import get_db
 from app.domain.repositories import HumanTaskRepository
 from app.schemas.human import (
     HumanTaskClaimRequest,
+    HumanTaskClaimPostRequest,
     HumanTaskCompleteRequest,
+    HumanTaskCompletePostRequest,
     HumanTaskCreateRequest,
+    HumanTaskDetailRequest,
+    HumanTaskListRequest,
     HumanTaskResponse,
 )
 
@@ -50,6 +54,20 @@ def list_human_tasks(
     ]
 
 
+@router.post("/tasks/list", response_model=list[HumanTaskResponse])
+def list_human_tasks_post(
+    payload: HumanTaskListRequest,
+    service: HumanApplicationService = Depends(get_human_application_service)) -> list[HumanTaskResponse]:
+    return [
+        HumanTaskResponse.from_entity(item)
+        for item in service.list_tasks(
+            status=payload.status,
+            assigned_to=payload.assigned_to,
+            run_id=payload.run_id,
+            limit=payload.limit)
+    ]
+
+
 @router.get("/tasks/{human_task_id}", response_model=HumanTaskResponse)
 def get_human_task(
     human_task_id: str,
@@ -60,6 +78,16 @@ def get_human_task(
     return HumanTaskResponse.from_entity(entity)
 
 
+@router.post("/tasks/detail", response_model=HumanTaskResponse)
+def get_human_task_post(
+    payload: HumanTaskDetailRequest,
+    service: HumanApplicationService = Depends(get_human_application_service)) -> HumanTaskResponse:
+    entity = service.get_task(human_task_id=payload.human_task_id)
+    if entity is None:
+        raise HTTPException(status_code=404, detail=f"human task not found: {payload.human_task_id}")
+    return HumanTaskResponse.from_entity(entity)
+
+
 @router.post("/tasks/{human_task_id}/claim", response_model=HumanTaskResponse)
 def claim_human_task(
     human_task_id: str,
@@ -71,6 +99,18 @@ def claim_human_task(
     return HumanTaskResponse.from_entity(entity)
 
 
+@router.post("/tasks/claim", response_model=HumanTaskResponse)
+def claim_human_task_post(
+    payload: HumanTaskClaimPostRequest,
+    service: HumanApplicationService = Depends(get_human_application_service)) -> HumanTaskResponse:
+    entity = service.claim_task(
+        human_task_id=payload.human_task_id,
+        payload=HumanTaskClaimRequest(claimed_by=payload.claimed_by))
+    if entity is None:
+        raise HTTPException(status_code=404, detail=f"human task not found: {payload.human_task_id}")
+    return HumanTaskResponse.from_entity(entity)
+
+
 @router.post("/tasks/{human_task_id}/complete", response_model=HumanTaskResponse)
 def complete_human_task(
     human_task_id: str,
@@ -80,3 +120,17 @@ def complete_human_task(
     if entity is None:
         raise HTTPException(status_code=404, detail=f"human task not found: {human_task_id}")
     return HumanTaskResponse.from_entity(entity)
+
+
+@router.post("/tasks/complete", response_model=HumanTaskResponse)
+def complete_human_task_post(
+    payload: HumanTaskCompletePostRequest,
+    service: HumanApplicationService = Depends(get_human_application_service)) -> HumanTaskResponse:
+    entity = service.complete_task(
+        human_task_id=payload.human_task_id,
+        payload=HumanTaskCompleteRequest(
+            status=payload.status,
+            response_payload_json=payload.response_payload_json))
+    if entity is None:
+        raise HTTPException(status_code=404, detail=f"human task not found: {payload.human_task_id}")
+    return HumanTaskResponse.from_entity(entity)

+ 0 - 1
services/human-service/app/bootstrap/settings.py

@@ -4,4 +4,3 @@ from core_shared import ServiceSettings
 class HumanServiceSettings(ServiceSettings):
     service_name: str = "human-service"
     service_port: int = 8011
-    database_url: str = "sqlite:///./human_service.db"

+ 3 - 3
services/human-service/app/db/models/human_task.py

@@ -1,13 +1,13 @@
 from datetime import datetime
 
-from core_db import AuditMixin, Base, EntityMixin, VersionMixin
+from core_db import AuditMixin, Base, EntityMixin
 from core_shared import JSONValue
 from sqlalchemy import DateTime, String, Text
-from sqlalchemy.dialects.sqlite import JSON
+from sqlalchemy import JSON
 from sqlalchemy.orm import Mapped, mapped_column
 
 
-class HumanTask(EntityMixin, AuditMixin, VersionMixin, Base):
+class HumanTask(EntityMixin, AuditMixin, Base):
     __tablename__ = "human_task"
 
     task_type: Mapped[str] = mapped_column(String(32), index=True)

+ 19 - 0
services/human-service/app/schemas/human.py

@@ -12,15 +12,34 @@ class HumanTaskCreateRequest(HumanTaskCreateContract):
     pass
 
 
+class HumanTaskListRequest(BaseModel):
+    status: HumanTaskStatus | None = None
+    assigned_to: str | None = None
+    run_id: str | None = None
+    limit: int = Field(default=100, ge=1, le=500)
+
+
+class HumanTaskDetailRequest(BaseModel):
+    human_task_id: str
+
+
 class HumanTaskClaimRequest(BaseModel):
     claimed_by: str
 
 
+class HumanTaskClaimPostRequest(HumanTaskClaimRequest):
+    human_task_id: str
+
+
 class HumanTaskCompleteRequest(BaseModel):
     status: HumanTaskStatus
     response_payload_json: dict[str, JSONValue] = Field(default_factory=dict)
 
 
+class HumanTaskCompletePostRequest(HumanTaskCompleteRequest):
+    human_task_id: str
+
+
 class HumanTaskResponse(HumanTaskContract):
     @classmethod
     def from_entity(cls, entity: "HumanTask") -> "HumanTaskResponse":

+ 1 - 1
services/knowledge-service/alembic.ini

@@ -1,7 +1,7 @@
 [alembic]
 script_location = alembic
 prepend_sys_path = .
-sqlalchemy.url = sqlite:///./knowledge_service.db
+sqlalchemy.url = postgresql+psycopg://admin:hFOvG5UBeK5KIGhz5cQH@git.newpoint.work:5432/vectordb
 
 [loggers]
 keys = root,sqlalchemy,alembic

+ 15 - 2
services/knowledge-service/alembic/env.py

@@ -1,10 +1,16 @@
+import os
 from logging.config import fileConfig
 
 from alembic import context
 from app.db.models import Base
 from sqlalchemy import engine_from_config, pool
 
+SERVICE_VERSION_TABLE = "knowledge_alembic_version"
+
 config = context.config
+database_url = os.getenv("AGENT_PLATFORM_DATABASE_URL")
+if database_url:
+    config.set_main_option("sqlalchemy.url", database_url.replace("%", "%%"))
 
 if config.config_file_name is not None:
     fileConfig(config.config_file_name)
@@ -14,7 +20,11 @@ target_metadata = Base.metadata
 
 def run_migrations_offline() -> None:
     url = config.get_main_option("sqlalchemy.url")
-    context.configure(url=url, target_metadata=target_metadata, literal_binds=True)
+    context.configure(
+        url=url,
+        target_metadata=target_metadata,
+        literal_binds=True,
+        version_table=SERVICE_VERSION_TABLE)
     with context.begin_transaction():
         context.run_migrations()
 
@@ -25,7 +35,10 @@ def run_migrations_online() -> None:
         prefix="sqlalchemy.",
         poolclass=pool.NullPool)
     with connectable.connect() as connection:
-        context.configure(connection=connection, target_metadata=target_metadata)
+        context.configure(
+            connection=connection,
+            target_metadata=target_metadata,
+            version_table=SERVICE_VERSION_TABLE)
         with context.begin_transaction():
             context.run_migrations()
 

+ 9 - 12
services/knowledge-service/alembic/versions/20260427_0002_add_pgvector_embeddings.py

@@ -7,7 +7,6 @@ Create Date: 2026-04-27 10:30:00
 
 from collections.abc import Sequence
 
-import sqlalchemy as sa
 from alembic import op
 
 revision: str = "20260427_0002"
@@ -18,17 +17,15 @@ depends_on: Sequence[str] | None = None
 
 def upgrade() -> None:
     bind = op.get_bind()
-    if bind.dialect.name == "postgresql":
-        op.execute("CREATE EXTENSION IF NOT EXISTS vector")
-        op.execute("ALTER TABLE knowledge_chunk ADD COLUMN embedding_vector vector")
-        op.execute(
-            "CREATE INDEX IF NOT EXISTS ix_knowledge_chunk_embedding_vector_hnsw "
-            "ON knowledge_chunk USING hnsw (embedding_vector vector_cosine_ops)"
-        )
-    else:
-        op.add_column(
-            "knowledge_chunk",
-            sa.Column("embedding_vector", sa.Text(), nullable=True))
+    if bind.dialect.name != "postgresql":
+        raise RuntimeError("knowledge pgvector migration requires PostgreSQL")
+    op.execute("CREATE EXTENSION IF NOT EXISTS vector WITH SCHEMA public")
+    op.execute("SELECT set_config('search_path', current_schema() || ',public', false)")
+    op.execute("ALTER TABLE knowledge_chunk ADD COLUMN IF NOT EXISTS embedding_vector vector(32)")
+    op.execute(
+        "CREATE INDEX IF NOT EXISTS ix_knowledge_chunk_embedding_vector_hnsw "
+        "ON knowledge_chunk USING hnsw (embedding_vector vector_cosine_ops)"
+    )
 
 
 def downgrade() -> None:

+ 22 - 0
services/knowledge-service/alembic/versions/20260429_9001_remove_version_columns.py

@@ -0,0 +1,22 @@
+"""Remove business version schema artifacts.
+
+Revision ID: 20260429_9001_knowledge
+Revises: 20260427_0002
+Create Date: 2026-04-29 00:00:00.000000
+"""
+
+from alembic import op
+
+revision: str = "20260429_9001_knowledge"
+down_revision: str | None = "20260427_0002"
+branch_labels = None
+depends_on = None
+
+
+def upgrade() -> None:
+    op.execute("DO $$\nDECLARE\n    table_record record;\nBEGIN\n    FOR table_record IN\n        SELECT table_name\n        FROM information_schema.columns\n        WHERE table_schema = current_schema()\n          AND column_name = 'version'\n    LOOP\n        EXECUTE format('ALTER TABLE %I DROP COLUMN IF EXISTS version', table_record.table_name);\n    END LOOP;\nEND $$;")
+
+
+def downgrade() -> None:
+    # Business version tables and columns were intentionally removed.
+    pass

+ 405 - 79
services/knowledge-service/app/api/routes.py

@@ -1,32 +1,85 @@
+from datetime import datetime
+from typing import TypeVar
+
 from core_domain import ServiceHealth
-from fastapi import APIRouter, Depends, HTTPException, Query
+from fastapi import APIRouter, Depends, HTTPException
 from sqlalchemy import text
 from sqlalchemy.orm import Session
 
 from app.application.document_parsers import DocumentParseError
-from app.application.services import KnowledgeApplicationService
+from app.application.services import (
+    KnowledgeApplicationService,
+    KnowledgeIndexingError,
+    build_knowledge_application_service,
+)
 from app.bootstrap.settings import KnowledgeServiceSettings
 from app.db.session import get_db
-from app.domain.repositories import (
-    KnowledgeBaseRepository,
-    KnowledgeChunkRepository,
-    KnowledgeDocumentRepository,
-)
+from app.infrastructure.object_storage import ObjectStorageError
 from app.schemas.knowledge import (
-    KnowledgeBaseCreateRequest,
-    KnowledgeBaseResponse,
+    ApiResponse,
+    DeleteData,
+    KnowledgeBaseCreateRequestDto,
+    KnowledgeBaseDeleteRequestDto,
+    KnowledgeBaseDetailRequestDto,
+    KnowledgeBaseDto,
+    KnowledgeBaseListRequestDto,
+    KnowledgeBaseStatusRequestDto,
     KnowledgeBaseStatusUpdateRequest,
-    KnowledgeChunkResponse,
-    KnowledgeDocumentCreateRequest,
-    KnowledgeDocumentIngestResponse,
+    KnowledgeBaseUpdateRequestDto,
+    KnowledgeBaseReindexData,
+    KnowledgeBaseReindexRequestDto,
+    KnowledgeChunkDetailRequestDto,
+    KnowledgeChunkDeleteRequestDto,
+    KnowledgeChunkDto,
+    KnowledgeChunkListRequestDto,
+    KnowledgeDocumentCreateRequestDto,
+    KnowledgeDocumentContentData,
+    KnowledgeDocumentContentRequestDto,
+    KnowledgeDocumentDeleteRequestDto,
+    KnowledgeDocumentDetailRequestDto,
+    KnowledgeDocumentDto,
+    KnowledgeDocumentIngestData,
+    KnowledgeDocumentListRequestDto,
     KnowledgeDocumentParseRequest,
-    KnowledgeDocumentParseResponse,
-    KnowledgeDocumentResponse,
+    KnowledgeDocumentParseRequestDto,
+    KnowledgeDocumentParseData,
+    KnowledgeDocumentReindexRequestDto,
+    KnowledgeDocumentStorageStatusData,
+    KnowledgeDocumentStorageStatusRequestDto,
+    KnowledgeDocumentStatusRequestDto,
+    KnowledgeDocumentUpdateRequestDto,
+    KnowledgeIndexJobData,
+    KnowledgeIndexJobDetailRequestDto,
+    KnowledgeIndexJobListRequestDto,
+    KnowledgeIndexJobRetryRequestDto,
+    KnowledgeSearchRequestDto,
     KnowledgeSearchRequest,
-    KnowledgeSearchResultResponse,
+    KnowledgeSearchResultDto,
+    KnowledgeSettingsDto,
+    KnowledgeSettingsUpdateRequestDto,
+    KnowledgeStorageHealthData,
+    KnowledgeStorageHealthRequestDto,
+    PageResult,
 )
 
 router = APIRouter()
+T = TypeVar("T")
+
+
+def ok(data: T) -> ApiResponse[T]:
+    return ApiResponse[T](
+        data=data,
+        requestId="",
+        serverTime=datetime.utcnow())
+
+
+def paginate(items: list[T], *, page: int, page_size: int) -> PageResult[T]:
+    start = (page - 1) * page_size
+    return PageResult.from_items(
+        items=items[start:start + page_size],
+        total=len(items),
+        page=page,
+        page_size=page_size)
 
 
 def get_knowledge_settings() -> KnowledgeServiceSettings:
@@ -36,11 +89,7 @@ def get_knowledge_settings() -> KnowledgeServiceSettings:
 def get_knowledge_application_service(
     db: Session = Depends(get_db),
     settings: KnowledgeServiceSettings = Depends(get_knowledge_settings)) -> KnowledgeApplicationService:
-    return KnowledgeApplicationService(
-        settings=settings,
-        base_repository=KnowledgeBaseRepository(db),
-        document_repository=KnowledgeDocumentRepository(db),
-        chunk_repository=KnowledgeChunkRepository(db))
+    return build_knowledge_application_service(db=db, settings=settings)
 
 
 @router.get("/health", response_model=ServiceHealth)
@@ -49,84 +98,361 @@ def health_check(db: Session = Depends(get_db)) -> ServiceHealth:
     return ServiceHealth(service="knowledge-service", status="ok", database="ok")
 
 
-@router.post("/bases", response_model=KnowledgeBaseResponse)
-def create_base(
-    payload: KnowledgeBaseCreateRequest,
-    service: KnowledgeApplicationService = Depends(get_knowledge_application_service)) -> KnowledgeBaseResponse:
-    return KnowledgeBaseResponse.from_entity(service.create_base(payload))
+@router.post("/storage/health", response_model=ApiResponse[KnowledgeStorageHealthData])
+def storage_health_contract(
+    payload: KnowledgeStorageHealthRequestDto,
+    service: KnowledgeApplicationService = Depends(get_knowledge_application_service)) -> ApiResponse[KnowledgeStorageHealthData]:
+    try:
+        health = service.read_storage_health()
+    except ObjectStorageError as exc:
+        health = {
+            "backend": service.settings.object_storage_backend,
+            "bucket": service.settings.object_storage_bucket,
+            "available": False,
+            "message": str(exc),
+        }
+    return ok(KnowledgeStorageHealthData(
+        backend=str(health.get("backend") or ""),
+        bucket=str(health.get("bucket") or ""),
+        available=bool(health.get("available")),
+        message=str(health["message"]) if health.get("message") is not None else None,
+        checkedTime=datetime.utcnow()))
 
 
-@router.get("/bases", response_model=list[KnowledgeBaseResponse])
-def list_bases(
-    service: KnowledgeApplicationService = Depends(get_knowledge_application_service)) -> list[KnowledgeBaseResponse]:
-    return [
-        KnowledgeBaseResponse.from_entity(item)
-        for item in service.list_bases()
+@router.post("/bases/list", response_model=ApiResponse[PageResult[KnowledgeBaseDto]])
+def list_bases_contract(
+    payload: KnowledgeBaseListRequestDto,
+    service: KnowledgeApplicationService = Depends(get_knowledge_application_service)) -> ApiResponse[PageResult[KnowledgeBaseDto]]:
+    items = [
+        KnowledgeBaseDto.from_entity(item)
+        for item in service.list_bases_filtered(
+            keyword=payload.keyword,
+            status=payload.status)
     ]
+    return ok(paginate(items, page=payload.page, page_size=payload.pageSize))
+
 
+@router.post("/bases/create", response_model=ApiResponse[KnowledgeBaseDto])
+def create_base_contract(
+    payload: KnowledgeBaseCreateRequestDto,
+    service: KnowledgeApplicationService = Depends(get_knowledge_application_service)) -> ApiResponse[KnowledgeBaseDto]:
+    return ok(KnowledgeBaseDto.from_entity(service.create_base_from_contract(payload)))
 
-@router.patch("/bases/{knowledge_base_id}/status", response_model=KnowledgeBaseResponse)
-def update_base_status(
-    knowledge_base_id: str,
-    payload: KnowledgeBaseStatusUpdateRequest,
-    service: KnowledgeApplicationService = Depends(get_knowledge_application_service)) -> KnowledgeBaseResponse:
+
+@router.post("/bases/detail", response_model=ApiResponse[KnowledgeBaseDto])
+def detail_base_contract(
+    payload: KnowledgeBaseDetailRequestDto,
+    service: KnowledgeApplicationService = Depends(get_knowledge_application_service)) -> ApiResponse[KnowledgeBaseDto]:
+    entity = service.base_repository.get_by_id(knowledge_base_id=payload.knowledgeBaseId)
+    if entity is None:
+        raise HTTPException(status_code=404, detail=f"knowledge base not found: {payload.knowledgeBaseId}")
+    return ok(KnowledgeBaseDto.from_entity(entity))
+
+
+@router.post("/bases/update", response_model=ApiResponse[KnowledgeBaseDto])
+def update_base_contract(
+    payload: KnowledgeBaseUpdateRequestDto,
+    service: KnowledgeApplicationService = Depends(get_knowledge_application_service)) -> ApiResponse[KnowledgeBaseDto]:
+    entity = service.update_base_from_contract(payload)
+    if entity is None:
+        raise HTTPException(status_code=404, detail=f"knowledge base not found: {payload.knowledgeBaseId}")
+    return ok(KnowledgeBaseDto.from_entity(entity))
+
+
+@router.post("/bases/status", response_model=ApiResponse[KnowledgeBaseDto])
+def update_base_status_contract(
+    payload: KnowledgeBaseStatusRequestDto,
+    service: KnowledgeApplicationService = Depends(get_knowledge_application_service)) -> ApiResponse[KnowledgeBaseDto]:
     entity = service.update_base_status(
-        knowledge_base_id=knowledge_base_id,
-        payload=payload)
+        knowledge_base_id=payload.knowledgeBaseId,
+        payload=KnowledgeBaseStatusUpdateRequest(status=payload.status))
+    if entity is None:
+        raise HTTPException(status_code=404, detail=f"knowledge base not found: {payload.knowledgeBaseId}")
+    return ok(KnowledgeBaseDto.from_entity(entity))
+
+
+@router.post("/bases/delete", response_model=ApiResponse[DeleteData])
+def delete_base_contract(
+    payload: KnowledgeBaseDeleteRequestDto,
+    service: KnowledgeApplicationService = Depends(get_knowledge_application_service)) -> ApiResponse[DeleteData]:
+    try:
+        deleted = service.delete_base(knowledge_base_id=payload.knowledgeBaseId)
+    except ObjectStorageError as exc:
+        raise HTTPException(status_code=503, detail=str(exc)) from exc
+    return ok(DeleteData(
+        deleted=deleted,
+        knowledgeBaseId=payload.knowledgeBaseId))
+
+
+@router.post("/documents/list", response_model=ApiResponse[PageResult[KnowledgeDocumentDto]])
+def list_documents_contract(
+    payload: KnowledgeDocumentListRequestDto,
+    service: KnowledgeApplicationService = Depends(get_knowledge_application_service)) -> ApiResponse[PageResult[KnowledgeDocumentDto]]:
+    items = [
+        KnowledgeDocumentDto.from_entity(item)
+        for item in service.list_documents_filtered(
+            knowledge_base_id=payload.knowledgeBaseId,
+            keyword=payload.keyword,
+            status=payload.status,
+            source_type=payload.sourceType)
+    ]
+    return ok(paginate(items, page=payload.page, page_size=payload.pageSize))
+
+
+@router.post("/documents/create", response_model=ApiResponse[KnowledgeDocumentIngestData])
+def create_document_contract(
+    payload: KnowledgeDocumentCreateRequestDto,
+    service: KnowledgeApplicationService = Depends(get_knowledge_application_service)) -> ApiResponse[KnowledgeDocumentIngestData]:
+    try:
+        result = service.create_document_from_contract_result(payload)
+    except KnowledgeIndexingError as exc:
+        raise HTTPException(status_code=503, detail=str(exc)) from exc
+    except ObjectStorageError as exc:
+        raise HTTPException(status_code=503, detail=str(exc)) from exc
+    except ValueError as exc:
+        raise HTTPException(status_code=422, detail=str(exc)) from exc
+    return ok(KnowledgeDocumentIngestData(
+        document=KnowledgeDocumentDto.from_entity(result.document),
+        chunks=[KnowledgeChunkDto.from_entity(item) for item in result.chunks],
+        queued=result.queued,
+        job=result.job))
+
+
+@router.post("/documents/detail", response_model=ApiResponse[KnowledgeDocumentDto])
+def detail_document_contract(
+    payload: KnowledgeDocumentDetailRequestDto,
+    service: KnowledgeApplicationService = Depends(get_knowledge_application_service)) -> ApiResponse[KnowledgeDocumentDto]:
+    entity = service.document_repository.get_by_id(document_id=payload.documentId)
+    if entity is None:
+        raise HTTPException(status_code=404, detail=f"knowledge document not found: {payload.documentId}")
+    return ok(KnowledgeDocumentDto.from_entity(entity))
+
+
+@router.post("/documents/update", response_model=ApiResponse[KnowledgeDocumentDto])
+def update_document_contract(
+    payload: KnowledgeDocumentUpdateRequestDto,
+    service: KnowledgeApplicationService = Depends(get_knowledge_application_service)) -> ApiResponse[KnowledgeDocumentDto]:
+    entity = service.update_document_from_contract(payload)
     if entity is None:
-        raise HTTPException(
-            status_code=404,
-            detail=f"knowledge base not found: {knowledge_base_id}")
-    return KnowledgeBaseResponse.from_entity(entity)
+        raise HTTPException(status_code=404, detail=f"knowledge document not found: {payload.documentId}")
+    return ok(KnowledgeDocumentDto.from_entity(entity))
 
 
-@router.post("/documents", response_model=KnowledgeDocumentIngestResponse)
-def create_document(
-    payload: KnowledgeDocumentCreateRequest,
-    service: KnowledgeApplicationService = Depends(get_knowledge_application_service)) -> KnowledgeDocumentIngestResponse:
+@router.post("/documents/status", response_model=ApiResponse[KnowledgeDocumentDto])
+def update_document_status_contract(
+    payload: KnowledgeDocumentStatusRequestDto,
+    service: KnowledgeApplicationService = Depends(get_knowledge_application_service)) -> ApiResponse[KnowledgeDocumentDto]:
+    entity = service.document_repository.update_status(
+        document_id=payload.documentId,
+        status=payload.status)
+    if entity is None:
+        raise HTTPException(status_code=404, detail=f"knowledge document not found: {payload.documentId}")
+    return ok(KnowledgeDocumentDto.from_entity(entity))
+
+
+@router.post("/documents/delete", response_model=ApiResponse[DeleteData])
+def delete_document_contract(
+    payload: KnowledgeDocumentDeleteRequestDto,
+    service: KnowledgeApplicationService = Depends(get_knowledge_application_service)) -> ApiResponse[DeleteData]:
     try:
-        document, chunks = service.create_document(payload)
+        result = service.delete_document_result(document_id=payload.documentId)
+    except ObjectStorageError as exc:
+        raise HTTPException(status_code=503, detail=str(exc)) from exc
+    return ok(DeleteData(
+        deleted=bool(result.get("deleted")),
+        documentId=payload.documentId,
+        objectDeleted=bool(result.get("objectDeleted"))))
+
+
+@router.post("/documents/reindex", response_model=ApiResponse[KnowledgeDocumentIngestData])
+def reindex_document_contract(
+    payload: KnowledgeDocumentReindexRequestDto,
+    service: KnowledgeApplicationService = Depends(get_knowledge_application_service)) -> ApiResponse[KnowledgeDocumentIngestData]:
+    try:
+        result = service.reindex_document_from_contract_result(payload)
+    except KnowledgeIndexingError as exc:
+        raise HTTPException(status_code=503, detail=str(exc)) from exc
+    except ObjectStorageError as exc:
+        raise HTTPException(status_code=503, detail=str(exc)) from exc
     except ValueError as exc:
         raise HTTPException(status_code=422, detail=str(exc)) from exc
-    return KnowledgeDocumentIngestResponse(
-        document=KnowledgeDocumentResponse.from_entity(document),
-        chunks=[KnowledgeChunkResponse.from_entity(item) for item in chunks])
+    if result is None:
+        raise HTTPException(status_code=404, detail=f"knowledge document not found: {payload.documentId}")
+    return ok(KnowledgeDocumentIngestData(
+        document=KnowledgeDocumentDto.from_entity(result.document),
+        chunks=[KnowledgeChunkDto.from_entity(item) for item in result.chunks],
+        queued=result.queued,
+        job=result.job))
+
+
+@router.post("/jobs/list", response_model=ApiResponse[PageResult[KnowledgeIndexJobData]])
+def list_index_jobs_contract(
+    payload: KnowledgeIndexJobListRequestDto,
+    service: KnowledgeApplicationService = Depends(get_knowledge_application_service)) -> ApiResponse[PageResult[KnowledgeIndexJobData]]:
+    items = service.list_index_jobs(
+        knowledge_base_id=payload.knowledgeBaseId,
+        document_id=payload.documentId,
+        status=payload.status)
+    return ok(paginate(items, page=payload.page, page_size=payload.pageSize))
+
 
+@router.post("/jobs/detail", response_model=ApiResponse[KnowledgeIndexJobData])
+def detail_index_job_contract(
+    payload: KnowledgeIndexJobDetailRequestDto,
+    service: KnowledgeApplicationService = Depends(get_knowledge_application_service)) -> ApiResponse[KnowledgeIndexJobData]:
+    job = service.detail_index_job(document_id=payload.documentId)
+    if job is None:
+        raise HTTPException(status_code=404, detail=f"knowledge index job not found for document: {payload.documentId}")
+    return ok(job)
 
-@router.post("/documents/parse", response_model=KnowledgeDocumentParseResponse)
-def parse_document(
-    payload: KnowledgeDocumentParseRequest,
-    service: KnowledgeApplicationService = Depends(get_knowledge_application_service)) -> KnowledgeDocumentParseResponse:
+
+@router.post("/jobs/retry", response_model=ApiResponse[KnowledgeDocumentIngestData])
+def retry_index_job_contract(
+    payload: KnowledgeIndexJobRetryRequestDto,
+    service: KnowledgeApplicationService = Depends(get_knowledge_application_service)) -> ApiResponse[KnowledgeDocumentIngestData]:
+    result = service.reindex_document_from_contract_result(
+        KnowledgeDocumentReindexRequestDto(
+            documentId=payload.documentId,
+            chunkSize=payload.chunkSize,
+            chunkOverlap=payload.chunkOverlap,
+            asyncMode=True))
+    if result is None:
+        raise HTTPException(status_code=404, detail=f"knowledge document not found: {payload.documentId}")
+    return ok(KnowledgeDocumentIngestData(
+        document=KnowledgeDocumentDto.from_entity(result.document),
+        chunks=[KnowledgeChunkDto.from_entity(item) for item in result.chunks],
+        queued=result.queued,
+        job=result.job))
+
+
+@router.post("/jobs/reindex-base", response_model=ApiResponse[KnowledgeBaseReindexData])
+def reindex_base_contract(
+    payload: KnowledgeBaseReindexRequestDto,
+    service: KnowledgeApplicationService = Depends(get_knowledge_application_service)) -> ApiResponse[KnowledgeBaseReindexData]:
+    if service.base_repository.get_by_id(knowledge_base_id=payload.knowledgeBaseId) is None:
+        raise HTTPException(status_code=404, detail=f"knowledge base not found: {payload.knowledgeBaseId}")
+    jobs = service.reindex_base_from_contract(payload)
+    return ok(KnowledgeBaseReindexData(
+        knowledgeBaseId=payload.knowledgeBaseId,
+        queuedCount=len(jobs),
+        jobs=jobs))
+
+
+@router.post("/documents/content", response_model=ApiResponse[KnowledgeDocumentContentData])
+def read_document_content_contract(
+    payload: KnowledgeDocumentContentRequestDto,
+    service: KnowledgeApplicationService = Depends(get_knowledge_application_service)) -> ApiResponse[KnowledgeDocumentContentData]:
     try:
-        parsed = service.parse_document(payload)
+        result = service.read_document_content(
+            document_id=payload.documentId,
+            include_text=payload.includeText,
+            include_base64=payload.includeBase64)
+    except ObjectStorageError as exc:
+        raise HTTPException(status_code=503, detail=str(exc)) from exc
+    except ValueError as exc:
+        raise HTTPException(status_code=422, detail=str(exc)) from exc
+    if result is None:
+        raise HTTPException(status_code=404, detail=f"knowledge document not found: {payload.documentId}")
+    return ok(KnowledgeDocumentContentData.model_validate(result))
+
+
+@router.post("/documents/storage/status", response_model=ApiResponse[KnowledgeDocumentStorageStatusData])
+def read_document_storage_status_contract(
+    payload: KnowledgeDocumentStorageStatusRequestDto,
+    service: KnowledgeApplicationService = Depends(get_knowledge_application_service)) -> ApiResponse[KnowledgeDocumentStorageStatusData]:
+    try:
+        result = service.read_document_storage_status(document_id=payload.documentId)
+    except ObjectStorageError as exc:
+        raise HTTPException(status_code=503, detail=str(exc)) from exc
+    if result is None:
+        raise HTTPException(status_code=404, detail=f"knowledge document not found: {payload.documentId}")
+    return ok(KnowledgeDocumentStorageStatusData.model_validate({
+        **result,
+        "checkedTime": datetime.utcnow(),
+    }))
+
+
+@router.post("/documents/parse", response_model=ApiResponse[KnowledgeDocumentParseData])
+def parse_document_contract(
+    payload: KnowledgeDocumentParseRequestDto,
+    service: KnowledgeApplicationService = Depends(get_knowledge_application_service)) -> ApiResponse[KnowledgeDocumentParseData]:
+    try:
+        parsed = service.parse_document(
+            KnowledgeDocumentParseRequest(
+                source_type=payload.sourceType,
+                source_uri=payload.sourceUri,
+                content_text=payload.contentText,
+                content_base64=payload.contentBase64))
     except DocumentParseError as exc:
         raise HTTPException(status_code=422, detail=str(exc)) from exc
-    return KnowledgeDocumentParseResponse(
-        content_text=parsed.content_text,
-        source_type=parsed.source_type,
-        metadata_json=parsed.metadata_json)
-
-
-@router.get("/documents", response_model=list[KnowledgeDocumentResponse])
-def list_documents(
-    knowledge_base_id: str = Query(...),
-    service: KnowledgeApplicationService = Depends(get_knowledge_application_service)) -> list[KnowledgeDocumentResponse]:
-    return [
-        KnowledgeDocumentResponse.from_entity(item)
-        for item in service.list_documents(
-            knowledge_base_id=knowledge_base_id)
+    return ok(KnowledgeDocumentParseData(
+        contentText=parsed.content_text,
+        sourceType=parsed.source_type,
+        metadata=parsed.metadata_json))
+
+
+@router.post("/chunks/list", response_model=ApiResponse[PageResult[KnowledgeChunkDto]])
+def list_chunks_contract(
+    payload: KnowledgeChunkListRequestDto,
+    service: KnowledgeApplicationService = Depends(get_knowledge_application_service)) -> ApiResponse[PageResult[KnowledgeChunkDto]]:
+    items = [
+        KnowledgeChunkDto.from_entity(item)
+        for item in service.list_chunks_filtered(
+            knowledge_base_id=payload.knowledgeBaseId,
+            document_id=payload.documentId,
+            keyword=payload.keyword)
     ]
+    return ok(paginate(items, page=payload.page, page_size=payload.pageSize))
+
+
+@router.post("/chunks/detail", response_model=ApiResponse[KnowledgeChunkDto])
+def detail_chunk_contract(
+    payload: KnowledgeChunkDetailRequestDto,
+    service: KnowledgeApplicationService = Depends(get_knowledge_application_service)) -> ApiResponse[KnowledgeChunkDto]:
+    entity = service.chunk_repository.get_by_id(chunk_id=payload.chunkId)
+    if entity is None:
+        raise HTTPException(status_code=404, detail=f"knowledge chunk not found: {payload.chunkId}")
+    return ok(KnowledgeChunkDto.from_entity(entity))
+
+
+@router.post("/chunks/delete", response_model=ApiResponse[DeleteData])
+def delete_chunk_contract(
+    payload: KnowledgeChunkDeleteRequestDto,
+    service: KnowledgeApplicationService = Depends(get_knowledge_application_service)) -> ApiResponse[DeleteData]:
+    return ok(DeleteData(
+        deleted=service.delete_chunk(chunk_id=payload.chunkId),
+        chunkId=payload.chunkId))
 
 
-@router.post("/search", response_model=list[KnowledgeSearchResultResponse])
-def search(
-    payload: KnowledgeSearchRequest,
-    service: KnowledgeApplicationService = Depends(get_knowledge_application_service)) -> list[KnowledgeSearchResultResponse]:
-    return [
-        KnowledgeSearchResultResponse(
-            chunk=KnowledgeChunkResponse.from_entity(chunk),
-            document=KnowledgeDocumentResponse.from_entity(document),
+@router.post("/search/query", response_model=ApiResponse[list[KnowledgeSearchResultDto]])
+def search_contract(
+    payload: KnowledgeSearchRequestDto,
+    service: KnowledgeApplicationService = Depends(get_knowledge_application_service)) -> ApiResponse[list[KnowledgeSearchResultDto]]:
+    results = [
+        KnowledgeSearchResultDto(
+            chunk=KnowledgeChunkDto.from_entity(chunk),
+            document=KnowledgeDocumentDto.from_entity(document),
             score=score,
-            score_json=score_json)
-        for chunk, document, score, score_json in service.search(payload)
+            scoreDetails=score_json)
+        for chunk, document, score, score_json in service.search(
+            KnowledgeSearchRequest(
+                knowledge_base_id=payload.knowledgeBaseId,
+                query=payload.query,
+                top_k=payload.topK,
+                filters_json=payload.filters))
     ]
+    return ok(results)
+
+
+@router.post("/settings/detail", response_model=ApiResponse[KnowledgeSettingsDto])
+def detail_settings_contract(
+    payload: KnowledgeBaseDetailRequestDto,
+    service: KnowledgeApplicationService = Depends(get_knowledge_application_service)) -> ApiResponse[KnowledgeSettingsDto]:
+    return ok(service.read_settings(knowledge_base_id=payload.knowledgeBaseId))
+
+
+@router.post("/settings/update", response_model=ApiResponse[KnowledgeSettingsDto])
+def update_settings_contract(
+    payload: KnowledgeSettingsUpdateRequestDto,
+    service: KnowledgeApplicationService = Depends(get_knowledge_application_service)) -> ApiResponse[KnowledgeSettingsDto]:
+    return ok(service.update_settings(payload))

+ 11 - 0
services/knowledge-service/app/application/document_parsers.py

@@ -90,6 +90,17 @@ def parse_document_content(
         })
 
 
+def read_document_content_bytes(
+    *,
+    content_text: str | None = None,
+    content_base64: str | None = None) -> bytes:
+    if content_base64 is not None:
+        return _decode_content_bytes(content_base64)
+    if content_text is None:
+        raise DocumentParseError("content_text or content_base64 is required")
+    return content_text.encode("utf-8")
+
+
 def normalize_source_type(*, source_type: str, source_uri: str | None = None) -> str:
     value = source_type.strip().lower() if source_type else ""
     if value and value != "auto":

+ 1004 - 22
services/knowledge-service/app/application/services.py

@@ -1,9 +1,23 @@
-from core_shared import JSONValue
+from __future__ import annotations
+
+import base64
+import hashlib
+from dataclasses import dataclass
+from datetime import datetime, timedelta
+from typing import TYPE_CHECKING, cast
+from uuid import uuid4
+
+from sqlalchemy.orm import Session
+
+from core_shared import JSONValue, try_build_redis_client
+from core_shared.task_queue import KNOWLEDGE_DOCUMENT_QUEUE, TaskQueuePublisher
 
 from app.application.document_parsers import (
     DocumentParseError,
     ParsedDocument,
-    parse_document_content)
+    normalize_source_type,
+    parse_document_content,
+    read_document_content_bytes)
 from app.application.embeddings import EmbeddingService
 from app.application.retrieval import (
     build_chunk_payloads,
@@ -17,13 +31,48 @@ from app.domain.repositories import (
     KnowledgeBaseRepository,
     KnowledgeChunkRepository,
     KnowledgeDocumentRepository)
+from app.infrastructure.object_storage import (
+    KnowledgeObjectStorage,
+    ObjectStorageError,
+    ObjectStorageNotFoundError,
+    ObjectStorageStatus,
+    build_document_object_key,
+    build_object_storage)
 from app.schemas.knowledge import (
     KnowledgeBaseCreateRequest,
+    KnowledgeBaseCreateRequestDto,
     KnowledgeBaseStatusUpdateRequest,
+    KnowledgeBaseUpdateRequestDto,
+    KnowledgeBaseReindexRequestDto,
     KnowledgeDocumentCreateRequest,
+    KnowledgeDocumentCreateRequestDto,
+    KnowledgeIndexJobAction,
+    KnowledgeIndexJobData,
+    KnowledgeIndexJobStatus,
     KnowledgeDocumentParseRequest,
+    KnowledgeDocumentReindexRequestDto,
+    KnowledgeDocumentUpdateRequestDto,
+    KnowledgeSettingsDto,
+    KnowledgeSettingsUpdateRequestDto,
     KnowledgeSearchRequest)
 
+if TYPE_CHECKING:
+    from redis import Redis
+
+
+@dataclass(frozen=True, slots=True)
+class KnowledgeDocumentIngestResult:
+    document: KnowledgeDocument
+    chunks: list[KnowledgeChunk]
+    queued: bool = False
+    job: KnowledgeIndexJobData | None = None
+
+
+class KnowledgeIndexingError(RuntimeError):
+    def __init__(self, *, document_id: str, message: str) -> None:
+        super().__init__(message)
+        self.document_id = document_id
+
 
 class KnowledgeApplicationService:
     def __init__(
@@ -32,12 +81,24 @@ class KnowledgeApplicationService:
         settings: KnowledgeServiceSettings,
         base_repository: KnowledgeBaseRepository,
         document_repository: KnowledgeDocumentRepository,
-        chunk_repository: KnowledgeChunkRepository) -> None:
+        chunk_repository: KnowledgeChunkRepository,
+        object_storage: KnowledgeObjectStorage | None = None,
+        redis_client: Redis | None = None,
+        task_queue_publisher: TaskQueuePublisher | None = None) -> None:
         self.settings = settings
         self.base_repository = base_repository
         self.document_repository = document_repository
         self.chunk_repository = chunk_repository
         self.embedding_service = EmbeddingService(settings=settings)
+        self._object_storage = object_storage
+        self.redis_client = redis_client
+        self.task_queue_publisher = task_queue_publisher
+
+    @property
+    def object_storage(self) -> KnowledgeObjectStorage:
+        if self._object_storage is None:
+            self._object_storage = build_object_storage(self.settings)
+        return self._object_storage
 
     def create_base(self, payload: KnowledgeBaseCreateRequest) -> KnowledgeBase:
         return self.base_repository.create(
@@ -46,9 +107,48 @@ class KnowledgeApplicationService:
             description=payload.description,
             metadata_json=payload.metadata_json)
 
+    def create_base_from_contract(
+        self,
+        payload: KnowledgeBaseCreateRequestDto) -> KnowledgeBase:
+        return self.create_base(
+            KnowledgeBaseCreateRequest(
+                code=self._build_base_code(payload.name),
+                name=payload.name,
+                description=payload.description,
+                metadata_json=payload.metadata))
+
     def list_bases(self) -> list[KnowledgeBase]:
         return self.base_repository.list_all()
 
+    def list_bases_filtered(
+        self,
+        *,
+        keyword: str | None = None,
+        status: str | None = None) -> list[KnowledgeBase]:
+        return self.base_repository.list_filtered(
+            keyword=keyword,
+            status=status)
+
+    def update_base_from_contract(
+        self,
+        payload: KnowledgeBaseUpdateRequestDto) -> KnowledgeBase | None:
+        return self.base_repository.update(
+            knowledge_base_id=payload.knowledgeBaseId,
+            name=payload.name,
+            description=payload.description,
+            status=payload.status,
+            metadata_json=payload.metadata)
+
+    def delete_base(self, *, knowledge_base_id: str) -> bool:
+        documents = self.document_repository.list_by_base(
+            knowledge_base_id=knowledge_base_id)
+        for document in documents:
+            self._delete_document_object(document=document)
+        self.chunk_repository.delete_by_base(knowledge_base_id=knowledge_base_id)
+        for document in documents:
+            self.document_repository.delete(document_id=document.id)
+        return self.base_repository.delete(knowledge_base_id=knowledge_base_id)
+
     def update_base_status(
         self,
         *,
@@ -73,27 +173,143 @@ class KnowledgeApplicationService:
                 content_text=payload.content_text,
                 content_base64=payload.content_base64)
         )
-        metadata_json = {
-            **payload.metadata_json,
-            "parser_metadata": parsed.metadata_json,
-        }
-        document = self.document_repository.create(
+        raw_content = read_document_content_bytes(
+            content_text=payload.content_text,
+            content_base64=payload.content_base64)
+        object_key = build_document_object_key(
             knowledge_base_id=payload.knowledge_base_id,
-            title=payload.title,
             source_type=parsed.source_type,
-            source_uri=payload.source_uri,
-            content_text=parsed.content_text,
-            content_hash=stable_content_hash(parsed.content_text),
-            metadata_json=metadata_json)
-        chunks = self._index_document(
+            title=payload.title)
+        stored_object = self.object_storage.put_bytes(
+            object_key=object_key,
+            content=raw_content,
+            content_type=self._guess_content_type(source_type=parsed.source_type))
+        document: KnowledgeDocument | None = None
+        try:
+            metadata_json = {
+                **payload.metadata_json,
+                "parser_metadata": parsed.metadata_json,
+                "object_storage": stored_object.to_metadata(),
+            }
+            document = self.document_repository.create(
+                knowledge_base_id=payload.knowledge_base_id,
+                title=payload.title,
+                source_type=parsed.source_type,
+                source_uri=payload.source_uri,
+                content_text="",
+                content_hash=stable_content_hash(parsed.content_text),
+                metadata_json=metadata_json)
+            try:
+                chunks = self._index_document(
+                    document=document,
+                    content_text=parsed.content_text,
+                    chunk_size=payload.chunk_size,
+                    chunk_overlap=payload.chunk_overlap)
+            except Exception as exc:
+                self._mark_document_failed(document=document, message=str(exc))
+                raise KnowledgeIndexingError(
+                    document_id=document.id,
+                    message=f"knowledge document indexing failed: {exc}") from exc
+            indexed_document = self.document_repository.update_status(
+                document_id=document.id,
+                status="indexed")
+            return indexed_document or document, chunks
+        except Exception:
+            if document is None:
+                try:
+                    self.object_storage.delete_object(object_key=stored_object.object_key)
+                except ObjectStorageError:
+                    pass
+            raise
+
+    def create_document_from_contract(
+        self,
+        payload: KnowledgeDocumentCreateRequestDto) -> tuple[KnowledgeDocument, list[KnowledgeChunk]]:
+        return self.create_document(
+            KnowledgeDocumentCreateRequest(
+                knowledge_base_id=payload.knowledgeBaseId,
+                title=payload.title,
+                content_text=payload.contentText,
+                content_base64=payload.contentBase64,
+                source_type=payload.sourceType,
+                source_uri=payload.sourceUri,
+                metadata_json=payload.metadata,
+                chunk_size=payload.chunkSize,
+                chunk_overlap=payload.chunkOverlap))
+
+    def create_document_from_contract_result(
+        self,
+        payload: KnowledgeDocumentCreateRequestDto) -> KnowledgeDocumentIngestResult:
+        request = KnowledgeDocumentCreateRequest(
+            knowledge_base_id=payload.knowledgeBaseId,
+            title=payload.title,
+            content_text=payload.contentText,
+            content_base64=payload.contentBase64,
+            source_type=payload.sourceType,
+            source_uri=payload.sourceUri,
+            metadata_json=payload.metadata,
+            chunk_size=payload.chunkSize,
+            chunk_overlap=payload.chunkOverlap)
+        if self._should_queue_indexing(async_mode=payload.asyncMode):
+            return self.create_document_index_job(payload=request)
+        document, chunks = self.create_document(request)
+        return KnowledgeDocumentIngestResult(
             document=document,
-            content_text=parsed.content_text,
-            chunk_size=payload.chunk_size,
-            chunk_overlap=payload.chunk_overlap)
-        indexed_document = self.document_repository.update_status(
-            document_id=document.id,
-            status="indexed")
-        return indexed_document or document, chunks
+            chunks=chunks)
+
+    def create_document_index_job(
+        self,
+        payload: KnowledgeDocumentCreateRequest) -> KnowledgeDocumentIngestResult:
+        knowledge_base = self.base_repository.get_by_id(
+            knowledge_base_id=payload.knowledge_base_id)
+        if knowledge_base is None:
+            raise ValueError(f"knowledge base not found: {payload.knowledge_base_id}")
+
+        raw_content = read_document_content_bytes(
+            content_text=payload.content_text,
+            content_base64=payload.content_base64)
+        source_type = normalize_source_type(
+            source_type=payload.source_type,
+            source_uri=payload.source_uri)
+        object_key = build_document_object_key(
+            knowledge_base_id=payload.knowledge_base_id,
+            source_type=source_type,
+            title=payload.title)
+        stored_object = self.object_storage.put_bytes(
+            object_key=object_key,
+            content=raw_content,
+            content_type=self._guess_content_type(source_type=source_type))
+        document: KnowledgeDocument | None = None
+        try:
+            document = self.document_repository.create(
+                knowledge_base_id=payload.knowledge_base_id,
+                title=payload.title,
+                source_type=source_type,
+                source_uri=payload.source_uri,
+                content_text="",
+                content_hash=hashlib.sha256(raw_content).hexdigest(),
+                metadata_json={
+                    **payload.metadata_json,
+                    "object_storage": stored_object.to_metadata(),
+                },
+                status="draft")
+            queued_document, job = self.queue_document_indexing(
+                document=document,
+                action="index",
+                chunk_size=payload.chunk_size,
+                chunk_overlap=payload.chunk_overlap)
+        except Exception:
+            if document is None:
+                try:
+                    self.object_storage.delete_object(object_key=stored_object.object_key)
+                except ObjectStorageError:
+                    pass
+            raise
+        return KnowledgeDocumentIngestResult(
+            document=queued_document,
+            chunks=[],
+            queued=True,
+            job=job)
 
     def parse_document(self, payload: KnowledgeDocumentParseRequest) -> ParsedDocument:
         try:
@@ -105,6 +321,57 @@ class KnowledgeApplicationService:
         except DocumentParseError:
             raise
 
+    def queue_document_indexing(
+        self,
+        *,
+        document: KnowledgeDocument,
+        action: str,
+        chunk_size: int | None = None,
+        chunk_overlap: int | None = None) -> tuple[KnowledgeDocument, KnowledgeIndexJobData]:
+        job_id = f"kjob_{uuid4().hex}"
+        metadata = self._write_index_job_metadata(
+            document=document,
+            action=action,
+            job_id=job_id,
+            status="queued",
+            progress=0,
+            chunk_size=chunk_size,
+            chunk_overlap=chunk_overlap)
+        updated_document = self.document_repository.update(
+            document_id=document.id,
+            status="queued",
+            metadata_json=metadata)
+        document_for_job = updated_document or document
+        published = self._publish_document_index_job(
+            document_id=document.id,
+            action=action,
+            job_id=job_id)
+        if not published:
+            metadata = self._write_index_job_metadata(
+                document=document_for_job,
+                action=action,
+                job_id=job_id,
+                status="running",
+                progress=1,
+                chunk_size=chunk_size,
+                chunk_overlap=chunk_overlap,
+                worker_key="inline-fallback")
+            document_for_job = self.document_repository.update(
+                document_id=document.id,
+                status="indexing",
+                metadata_json=metadata) or document_for_job
+            processed = self.process_document_index_job(
+                document_id=document.id,
+                action=action,
+                job_id=job_id,
+                worker_key="inline-fallback",
+                chunk_size=chunk_size,
+                chunk_overlap=chunk_overlap)
+            if processed is not None:
+                indexed_document, chunks = processed
+                return indexed_document, self._read_latest_index_job(document=indexed_document)
+        return document_for_job, self._read_latest_index_job(document=document_for_job)
+
     def list_documents(
         self,
         *,
@@ -112,6 +379,378 @@ class KnowledgeApplicationService:
         return self.document_repository.list_by_base(
             knowledge_base_id=knowledge_base_id)
 
+    def list_documents_filtered(
+        self,
+        *,
+        knowledge_base_id: str | None = None,
+        keyword: str | None = None,
+        status: str | None = None,
+        source_type: str | None = None) -> list[KnowledgeDocument]:
+        return self.document_repository.list_filtered(
+            knowledge_base_id=knowledge_base_id,
+            keyword=keyword,
+            status=status,
+            source_type=source_type)
+
+    def update_document_from_contract(
+        self,
+        payload: KnowledgeDocumentUpdateRequestDto) -> KnowledgeDocument | None:
+        return self.document_repository.update(
+            document_id=payload.documentId,
+            title=payload.title,
+            source_uri=payload.sourceUri,
+            status=payload.status,
+            metadata_json=payload.metadata)
+
+    def reindex_document(
+        self,
+        payload: KnowledgeDocumentReindexRequestDto) -> tuple[KnowledgeDocument, list[KnowledgeChunk]] | None:
+        document = self.document_repository.get_by_id(document_id=payload.documentId)
+        if document is None:
+            return None
+        try:
+            parsed = self._parse_document_for_indexing(document=document)
+            chunks = self._index_document(
+                document=document,
+                content_text=parsed.content_text,
+                chunk_size=payload.chunkSize,
+                chunk_overlap=payload.chunkOverlap)
+        except Exception as exc:
+            self._mark_document_failed(document=document, message=str(exc))
+            raise KnowledgeIndexingError(
+                document_id=document.id,
+                message=f"knowledge document reindex failed: {exc}") from exc
+        metadata = dict(document.metadata_json or {})
+        metadata["parser_metadata"] = parsed.metadata_json
+        indexed_document = self.document_repository.update(
+            document_id=document.id,
+            status="indexed",
+            metadata_json=metadata)
+        return indexed_document or document, chunks
+
+    def reindex_document_from_contract_result(
+        self,
+        payload: KnowledgeDocumentReindexRequestDto) -> KnowledgeDocumentIngestResult | None:
+        if self._should_queue_indexing(async_mode=payload.asyncMode):
+            document = self.document_repository.get_by_id(document_id=payload.documentId)
+            if document is None:
+                return None
+            queued_document, job = self.queue_document_indexing(
+                document=document,
+                action="reindex",
+                chunk_size=payload.chunkSize,
+                chunk_overlap=payload.chunkOverlap)
+            return KnowledgeDocumentIngestResult(
+                document=queued_document,
+                chunks=[],
+                queued=True,
+                job=job)
+        result = self.reindex_document(payload)
+        if result is None:
+            return None
+        document, chunks = result
+        return KnowledgeDocumentIngestResult(
+            document=document,
+            chunks=chunks)
+
+    def reindex_base_from_contract(
+        self,
+        payload: KnowledgeBaseReindexRequestDto) -> list[KnowledgeIndexJobData]:
+        documents = self.document_repository.list_by_base(
+            knowledge_base_id=payload.knowledgeBaseId)
+        jobs: list[KnowledgeIndexJobData] = []
+        for document in documents:
+            if document.status == "archived":
+                continue
+            queued_document, job = self.queue_document_indexing(
+                document=document,
+                action="reindex",
+                chunk_size=payload.chunkSize,
+                chunk_overlap=payload.chunkOverlap)
+            jobs.append(job or self._read_latest_index_job(document=queued_document))
+        return jobs
+
+    def process_document_index_job(
+        self,
+        *,
+        document_id: str,
+        action: str,
+        worker_key: str,
+        job_id: str | None = None,
+        chunk_size: int | None = None,
+        chunk_overlap: int | None = None) -> tuple[KnowledgeDocument, list[KnowledgeChunk]] | None:
+        document = self.document_repository.get_by_id(document_id=document_id)
+        if document is None:
+            return None
+        resolved_job = self._read_latest_index_job(document=document)
+        resolved_job_id = job_id or resolved_job.jobId
+        resolved_chunk_size = chunk_size if chunk_size is not None else resolved_job.chunkSize
+        resolved_chunk_overlap = chunk_overlap if chunk_overlap is not None else resolved_job.chunkOverlap
+        if document.status == "archived":
+            metadata = self._write_index_job_metadata(
+                document=document,
+                action=action,
+                job_id=resolved_job_id,
+                status="skipped",
+                progress=100,
+                chunk_size=resolved_chunk_size,
+                chunk_overlap=resolved_chunk_overlap,
+                worker_key=worker_key,
+                completed_time=datetime.utcnow(),
+                error_message="document is archived")
+            skipped_document = self.document_repository.update(
+                document_id=document.id,
+                metadata_json=metadata) or document
+            return skipped_document, []
+
+        metadata = self._write_index_job_metadata(
+            document=document,
+            action=action,
+            job_id=resolved_job_id,
+            status="running",
+            progress=10,
+            chunk_size=resolved_chunk_size,
+            chunk_overlap=resolved_chunk_overlap,
+            worker_key=worker_key,
+            started_time=datetime.utcnow())
+        running_document = self.document_repository.update(
+            document_id=document.id,
+            status="indexing",
+            metadata_json=metadata) or document
+        try:
+            parsed = self._parse_document_for_indexing(document=running_document)
+            metadata = self._write_index_job_metadata(
+                document=running_document,
+                action=action,
+                job_id=resolved_job_id,
+                status="running",
+                progress=40,
+                chunk_size=resolved_chunk_size,
+                chunk_overlap=resolved_chunk_overlap,
+                worker_key=worker_key,
+                started_time=self._read_latest_index_job(document=running_document).startedTime)
+            metadata["parser_metadata"] = parsed.metadata_json
+            running_document = self.document_repository.update(
+                document_id=document.id,
+                status="indexing",
+                metadata_json=metadata) or running_document
+            chunks = self._index_document(
+                document=running_document,
+                content_text=parsed.content_text,
+                chunk_size=resolved_chunk_size,
+                chunk_overlap=resolved_chunk_overlap)
+        except Exception as exc:
+            self._mark_document_failed(
+                document=running_document,
+                message=str(exc),
+                job_id=resolved_job_id,
+                action=action,
+                worker_key=worker_key,
+                chunk_size=resolved_chunk_size,
+                chunk_overlap=resolved_chunk_overlap)
+            raise KnowledgeIndexingError(
+                document_id=document.id,
+                message=f"knowledge document {action} failed: {exc}") from exc
+
+        metadata = self._write_index_job_metadata(
+            document=running_document,
+            action=action,
+            job_id=resolved_job_id,
+            status="completed",
+            progress=100,
+            chunk_size=resolved_chunk_size,
+            chunk_overlap=resolved_chunk_overlap,
+            worker_key=worker_key,
+            completed_time=datetime.utcnow())
+        metadata["parser_metadata"] = parsed.metadata_json
+        indexed_document = self.document_repository.update(
+            document_id=document.id,
+            status="indexed",
+            metadata_json=metadata)
+        return indexed_document or running_document, chunks
+
+    def execute_document_index_job(
+        self,
+        *,
+        document_id: str,
+        action: str,
+        worker_key: str,
+        lease_seconds: int,
+        job_id: str | None = None,
+        redis_client: Redis | None = None) -> tuple[KnowledgeDocument, list[KnowledgeChunk]] | None:
+        resolved_redis_client = redis_client or self.redis_client
+        idempotency_key = f"{document_id}:{job_id or action}"
+        lock = None
+        idempotency_store = None
+        if resolved_redis_client is not None:
+            from core_shared.redis_primitives import DistributedLock, IdempotencyStore
+
+            lock = DistributedLock(
+                client=resolved_redis_client,
+                name=f"knowledge-document:{document_id}:lock",
+                ttl_seconds=lease_seconds)
+            if not lock.acquire():
+                return None
+            idempotency_store = IdempotencyStore(
+                client=resolved_redis_client,
+                prefix="knowledge-document-idempotency")
+            if not idempotency_store.begin(key=idempotency_key):
+                lock.release()
+                return None
+        try:
+            result = self.process_document_index_job(
+                document_id=document_id,
+                action=action,
+                job_id=job_id,
+                worker_key=worker_key)
+            if idempotency_store is not None and result is not None:
+                document, chunks = result
+                idempotency_store.complete(
+                    key=idempotency_key,
+                    result={
+                        "status": document.status,
+                        "document_id": document.id,
+                        "chunk_count": len(chunks),
+                    })
+        except Exception:
+            if idempotency_store is not None:
+                idempotency_store.clear(key=idempotency_key)
+            raise
+        finally:
+            if lock is not None:
+                lock.release()
+        return result
+
+    def execute_next_pending_document_job(
+        self,
+        *,
+        worker_key: str,
+        lease_seconds: int,
+        stale_indexing_seconds: int,
+        redis_client: Redis | None = None) -> tuple[KnowledgeDocument, list[KnowledgeChunk]] | None:
+        stale_before = datetime.utcnow() - timedelta(seconds=stale_indexing_seconds)
+        document = self.document_repository.get_next_pending_indexing(
+            stale_before=stale_before)
+        if document is None:
+            return None
+        job = self._read_latest_index_job(document=document)
+        return self.execute_document_index_job(
+            document_id=document.id,
+            action=job.action,
+            job_id=job.jobId,
+            worker_key=worker_key,
+            lease_seconds=lease_seconds,
+            redis_client=redis_client)
+
+    def list_index_jobs(
+        self,
+        *,
+        knowledge_base_id: str | None = None,
+        document_id: str | None = None,
+        status: str | None = None) -> list[KnowledgeIndexJobData]:
+        documents = self.document_repository.list_filtered(
+            knowledge_base_id=knowledge_base_id)
+        jobs: list[KnowledgeIndexJobData] = []
+        for document in documents:
+            if document_id is not None and document.id != document_id:
+                continue
+            job = self._try_read_latest_index_job(document=document)
+            if job is None:
+                continue
+            if status is not None and job.status != status:
+                continue
+            jobs.append(job)
+        jobs.sort(
+            key=lambda item: item.queuedTime or datetime.min,
+            reverse=True)
+        return jobs
+
+    def detail_index_job(self, *, document_id: str) -> KnowledgeIndexJobData | None:
+        document = self.document_repository.get_by_id(document_id=document_id)
+        if document is None:
+            return None
+        return self._try_read_latest_index_job(document=document)
+
+    def delete_document(self, *, document_id: str) -> bool:
+        return bool(self.delete_document_result(document_id=document_id)["deleted"])
+
+    def delete_document_result(self, *, document_id: str) -> dict[str, JSONValue]:
+        document = self.document_repository.get_by_id(document_id=document_id)
+        if document is None:
+            return {
+                "deleted": False,
+                "objectDeleted": False,
+                "documentId": document_id,
+            }
+        object_deleted = self._delete_document_object(document=document)
+        self.chunk_repository.delete_by_document(document_id=document_id)
+        deleted = self.document_repository.delete(document_id=document_id) is not None
+        return {
+            "deleted": deleted,
+            "objectDeleted": object_deleted,
+            "documentId": document_id,
+        }
+
+    def read_document_content(
+        self,
+        *,
+        document_id: str,
+        include_text: bool = True,
+        include_base64: bool = False) -> dict[str, JSONValue] | None:
+        document = self.document_repository.get_by_id(document_id=document_id)
+        if document is None:
+            return None
+        raw_content = self._read_document_raw_content(document=document)
+        object_status = self.read_document_storage_status(document_id=document_id)
+        content_type = self._read_content_type_from_status(object_status)
+        payload: dict[str, JSONValue] = {
+            "documentId": document.id,
+            "title": document.title,
+            "sourceType": document.source_type,
+            "contentType": content_type,
+            "sizeBytes": len(raw_content),
+            "objectStorage": self._read_object_storage_metadata(document=document),
+            "contentBase64": None,
+            "contentText": None,
+        }
+        if include_base64:
+            payload["contentBase64"] = base64.b64encode(raw_content).decode("ascii")
+        if include_text and self._is_text_content_type(content_type=content_type, source_type=document.source_type):
+            payload["contentText"] = raw_content.decode("utf-8", errors="replace")
+        return payload
+
+    def read_document_storage_status(self, *, document_id: str) -> dict[str, JSONValue] | None:
+        document = self.document_repository.get_by_id(document_id=document_id)
+        if document is None:
+            return None
+        object_key = self._read_document_object_key(document=document)
+        if object_key is None:
+            return {
+                "documentId": document.id,
+                "exists": False,
+                "objectStorage": None,
+                "errorMessage": "document object metadata is missing",
+            }
+        status = self.object_storage.head_object(object_key=object_key)
+        return self._object_status_to_payload(document=document, status=status)
+
+    def read_storage_health(self) -> dict[str, JSONValue]:
+        return dict(self.object_storage.health_check())
+
+    def list_chunks_filtered(
+        self,
+        *,
+        knowledge_base_id: str | None = None,
+        document_id: str | None = None,
+        keyword: str | None = None) -> list[KnowledgeChunk]:
+        return self.chunk_repository.list_filtered(
+            knowledge_base_id=knowledge_base_id,
+            document_id=document_id,
+            keyword=keyword)
+
+    def delete_chunk(self, *, chunk_id: str) -> bool:
+        return self.chunk_repository.delete(chunk_id=chunk_id)
+
     def search(
         self,
         payload: KnowledgeSearchRequest) -> list[tuple[KnowledgeChunk, KnowledgeDocument, float, dict[str, JSONValue]]]:
@@ -223,15 +862,358 @@ class KnowledgeApplicationService:
         value = chunk_payload.get("content_text")
         return value if isinstance(value, str) else ""
 
+    def _parse_document_for_indexing(self, *, document: KnowledgeDocument) -> ParsedDocument:
+        metadata = document.metadata_json or {}
+        object_metadata = metadata.get("object_storage")
+        if isinstance(object_metadata, dict):
+            object_key = object_metadata.get("objectKey")
+            if isinstance(object_key, str) and object_key:
+                try:
+                    raw_content = self.object_storage.get_bytes(object_key=object_key)
+                except ObjectStorageNotFoundError as exc:
+                    raise ValueError(f"knowledge document content object not found: {document.id}") from exc
+                return parse_document_content(
+                    source_type=document.source_type,
+                    source_uri=document.source_uri,
+                    content_base64=base64.b64encode(raw_content).decode("ascii"))
+        if document.content_text:
+            return parse_document_content(
+                source_type=document.source_type,
+                source_uri=document.source_uri,
+                content_text=document.content_text)
+        raise ValueError(f"knowledge document content object not found: {document.id}")
+
+    def _read_document_content_for_indexing(self, *, document: KnowledgeDocument) -> str:
+        return self._parse_document_for_indexing(document=document).content_text
+
+    def _read_document_raw_content(self, *, document: KnowledgeDocument) -> bytes:
+        object_key = self._read_document_object_key(document=document)
+        if isinstance(object_key, str) and object_key:
+            return self.object_storage.get_bytes(object_key=object_key)
+        if document.content_text:
+            return document.content_text.encode("utf-8")
+        raise ValueError(f"knowledge document content object not found: {document.id}")
+
+    def _delete_document_object(self, *, document: KnowledgeDocument) -> bool:
+        object_key = self._read_document_object_key(document=document)
+        if object_key is None:
+            return False
+        try:
+            return self.object_storage.delete_object(object_key=object_key)
+        except ObjectStorageNotFoundError:
+            return False
+
+    def _read_document_object_key(self, *, document: KnowledgeDocument) -> str | None:
+        object_metadata = self._read_object_storage_metadata(document=document)
+        if object_metadata is None:
+            return None
+        object_key = object_metadata.get("objectKey")
+        return object_key if isinstance(object_key, str) and object_key else None
+
+    def _read_object_storage_metadata(
+        self,
+        *,
+        document: KnowledgeDocument) -> dict[str, JSONValue] | None:
+        metadata = document.metadata_json or {}
+        object_metadata = metadata.get("object_storage")
+        return object_metadata if isinstance(object_metadata, dict) else None
+
+    def _object_status_to_payload(
+        self,
+        *,
+        document: KnowledgeDocument,
+        status: ObjectStorageStatus) -> dict[str, JSONValue]:
+        return {
+            "documentId": document.id,
+            "exists": status.exists,
+            "objectStorage": self._read_object_storage_metadata(document=document),
+            "contentType": status.content_type,
+            "sizeBytes": status.size_bytes,
+            "etag": status.etag,
+            "errorMessage": status.error_message,
+        }
+
+    def _read_content_type_from_status(
+        self,
+        object_status: dict[str, JSONValue] | None) -> str | None:
+        if object_status is None:
+            return None
+        content_type = object_status.get("contentType")
+        return content_type if isinstance(content_type, str) else None
+
+    def _is_text_content_type(self, *, content_type: str | None, source_type: str) -> bool:
+        if content_type is not None and content_type.startswith("text/"):
+            return True
+        return source_type.strip().lower() in {"text", "txt", "markdown", "md", "html", "htm", "json", "csv"}
+
+    def _should_queue_indexing(self, *, async_mode: bool | None) -> bool:
+        if async_mode is False:
+            return False
+        if not self.settings.async_indexing_enabled and async_mode is None:
+            return False
+        return self.task_queue_publisher is not None
+
+    def _publish_document_index_job(
+        self,
+        *,
+        document_id: str,
+        action: str,
+        job_id: str) -> bool:
+        if self.task_queue_publisher is None:
+            return False
+        return self.task_queue_publisher.publish_knowledge_document(
+            document_id=document_id,
+            action=action,
+            job_id=job_id)
+
+    def _write_index_job_metadata(
+        self,
+        *,
+        document: KnowledgeDocument,
+        action: str,
+        job_id: str,
+        status: str,
+        progress: int,
+        chunk_size: int | None = None,
+        chunk_overlap: int | None = None,
+        worker_key: str | None = None,
+        started_time: datetime | None = None,
+        completed_time: datetime | None = None,
+        error_message: str | None = None) -> dict[str, JSONValue]:
+        metadata = dict(document.metadata_json or {})
+        existing_job = self._read_index_job_payload(document=document)
+        queued_time = existing_job.get("queuedTime")
+        if not isinstance(queued_time, str):
+            queued_time = datetime.utcnow().isoformat()
+        resolved_started_time = (
+            started_time.isoformat()
+            if started_time is not None
+            else existing_job.get("startedTime")
+        )
+        resolved_completed_time = (
+            completed_time.isoformat()
+            if completed_time is not None
+            else existing_job.get("completedTime")
+        )
+        job_payload: dict[str, JSONValue] = {
+            "jobId": job_id,
+            "documentId": document.id,
+            "knowledgeBaseId": document.knowledge_base_id,
+            "documentTitle": document.title,
+            "action": action if action in {"index", "reindex"} else "reindex",
+            "status": status,
+            "progress": max(0, min(progress, 100)),
+            "queueName": KNOWLEDGE_DOCUMENT_QUEUE,
+            "workerKey": worker_key,
+            "errorMessage": error_message,
+            "chunkSize": chunk_size,
+            "chunkOverlap": chunk_overlap,
+            "queuedTime": queued_time,
+            "startedTime": resolved_started_time if isinstance(resolved_started_time, str) else None,
+            "completedTime": resolved_completed_time if isinstance(resolved_completed_time, str) else None,
+        }
+        metadata["index_job"] = job_payload
+        return metadata
+
+    def _read_latest_index_job(self, *, document: KnowledgeDocument) -> KnowledgeIndexJobData:
+        payload = self._read_index_job_payload(document=document)
+        return KnowledgeIndexJobData(
+            jobId=self._read_payload_string(payload, "jobId") or f"kjob_{document.id}",
+            documentId=self._read_payload_string(payload, "documentId") or document.id,
+            knowledgeBaseId=self._read_payload_string(payload, "knowledgeBaseId") or document.knowledge_base_id,
+            documentTitle=self._read_payload_string(payload, "documentTitle") or document.title,
+            action=self._read_job_action(payload.get("action")),
+            status=self._read_job_status(payload.get("status")),
+            progress=self._read_payload_int(payload, "progress", 0),
+            queueName=self._read_payload_string(payload, "queueName"),
+            workerKey=self._read_payload_string(payload, "workerKey"),
+            errorMessage=self._read_payload_string(payload, "errorMessage"),
+            chunkSize=self._read_optional_payload_int(payload, "chunkSize"),
+            chunkOverlap=self._read_optional_payload_int(payload, "chunkOverlap"),
+            queuedTime=self._read_payload_datetime(payload, "queuedTime"),
+            startedTime=self._read_payload_datetime(payload, "startedTime"),
+            completedTime=self._read_payload_datetime(payload, "completedTime"))
+
+    def _try_read_latest_index_job(self, *, document: KnowledgeDocument) -> KnowledgeIndexJobData | None:
+        if not self._read_index_job_payload(document=document):
+            return None
+        return self._read_latest_index_job(document=document)
+
+    def _read_index_job_payload(self, *, document: KnowledgeDocument) -> dict[str, JSONValue]:
+        metadata = document.metadata_json or {}
+        value = metadata.get("index_job")
+        if isinstance(value, dict):
+            return {str(item_key): item_value for item_key, item_value in value.items()}
+        return {}
+
+    def _read_payload_string(
+        self,
+        payload: dict[str, JSONValue],
+        key: str) -> str | None:
+        value = payload.get(key)
+        return value if isinstance(value, str) and value else None
+
+    def _read_payload_int(
+        self,
+        payload: dict[str, JSONValue],
+        key: str,
+        fallback: int) -> int:
+        value = payload.get(key)
+        if isinstance(value, int) and not isinstance(value, bool):
+            return value
+        return fallback
+
+    def _read_optional_payload_int(
+        self,
+        payload: dict[str, JSONValue],
+        key: str) -> int | None:
+        value = payload.get(key)
+        if isinstance(value, int) and not isinstance(value, bool):
+            return value
+        return None
+
+    def _read_payload_datetime(
+        self,
+        payload: dict[str, JSONValue],
+        key: str) -> datetime | None:
+        value = payload.get(key)
+        if not isinstance(value, str) or not value:
+            return None
+        try:
+            return datetime.fromisoformat(value)
+        except ValueError:
+            return None
+
+    def _read_job_action(self, value: JSONValue) -> KnowledgeIndexJobAction:
+        if isinstance(value, str) and value in {"index", "reindex"}:
+            return cast(KnowledgeIndexJobAction, value)
+        return "reindex"
+
+    def _read_job_status(self, value: JSONValue) -> KnowledgeIndexJobStatus:
+        if isinstance(value, str) and value in {"queued", "running", "completed", "failed", "skipped"}:
+            return cast(KnowledgeIndexJobStatus, value)
+        return "queued"
+
+    def _mark_document_failed(
+        self,
+        *,
+        document: KnowledgeDocument,
+        message: str,
+        job_id: str | None = None,
+        action: str = "reindex",
+        worker_key: str | None = None,
+        chunk_size: int | None = None,
+        chunk_overlap: int | None = None) -> None:
+        metadata = dict(document.metadata_json or {})
+        metadata["last_error"] = {
+            "message": message[:1000],
+            "errorType": "indexing_failed",
+        }
+        if job_id is not None:
+            metadata = self._write_index_job_metadata(
+                document=document,
+                action=action,
+                job_id=job_id,
+                status="failed",
+                progress=100,
+                chunk_size=chunk_size,
+                chunk_overlap=chunk_overlap,
+                worker_key=worker_key,
+                completed_time=datetime.utcnow(),
+                error_message=message[:1000])
+        self.document_repository.update(
+            document_id=document.id,
+            status="failed",
+            metadata_json=metadata)
+
+    def _guess_content_type(self, *, source_type: str) -> str:
+        normalized = source_type.strip().lower().removeprefix(".")
+        if normalized in {"markdown", "md"}:
+            return "text/markdown; charset=utf-8"
+        if normalized in {"html", "htm"}:
+            return "text/html; charset=utf-8"
+        if normalized == "json":
+            return "application/json"
+        if normalized == "csv":
+            return "text/csv; charset=utf-8"
+        if normalized == "pdf":
+            return "application/pdf"
+        if normalized in {"docx", "word"}:
+            return "application/vnd.openxmlformats-officedocument.wordprocessingml.document"
+        return "text/plain; charset=utf-8"
+
     def _matches_filters(
         self,
         *,
         document: KnowledgeDocument,
         filters_json: dict[str, JSONValue]) -> bool:
-        source_type = filters_json.get("source_type")
+        source_type = filters_json.get("sourceType") or filters_json.get("source_type")
         if isinstance(source_type, str) and document.source_type != source_type:
             return False
         status = filters_json.get("status")
         if isinstance(status, str) and document.status != status:
             return False
         return True
+
+    def read_settings(self, *, knowledge_base_id: str | None = None) -> KnowledgeSettingsDto:
+        base_config: dict[str, JSONValue] = {}
+        if knowledge_base_id:
+            base = self.base_repository.get_by_id(knowledge_base_id=knowledge_base_id)
+            if base is not None and isinstance(base.metadata_json, dict):
+                value = base.metadata_json.get("retrieval_config")
+                if isinstance(value, dict):
+                    base_config = value
+        defaults = KnowledgeSettingsDto(
+            knowledgeBaseId=knowledge_base_id,
+            chunkSize=self.settings.default_chunk_size,
+            chunkOverlap=self.settings.default_chunk_overlap,
+            keywordWeight=self.settings.retrieval_keyword_weight,
+            vectorWeight=self.settings.retrieval_vector_weight,
+            rerankWeight=self.settings.retrieval_rerank_weight,
+            queryRewrite=False,
+            requireCitations=True)
+        return KnowledgeSettingsDto.model_validate({
+            **defaults.model_dump(),
+            **base_config,
+            "knowledgeBaseId": knowledge_base_id,
+        })
+
+    def update_settings(
+        self,
+        payload: KnowledgeSettingsUpdateRequestDto) -> KnowledgeSettingsDto:
+        settings = KnowledgeSettingsDto.model_validate({
+            **payload.model_dump(),
+            "knowledgeBaseId": payload.knowledgeBaseId,
+        })
+        if payload.knowledgeBaseId:
+            base = self.base_repository.get_by_id(knowledge_base_id=payload.knowledgeBaseId)
+            if base is not None:
+                metadata = dict(base.metadata_json or {})
+                metadata["retrieval_config"] = settings.model_dump(exclude={"knowledgeBaseId"})
+                self.base_repository.update(
+                    knowledge_base_id=payload.knowledgeBaseId,
+                    metadata_json=metadata)
+        return settings
+
+    def _build_base_code(self, name: str) -> str:
+        base = "".join(
+            char.lower() if char.isalnum() else "_"
+            for char in name
+        ).strip("_") or "knowledge_base"
+        return base[:64]
+
+
+def build_knowledge_application_service(
+    *,
+    db: Session,
+    settings: KnowledgeServiceSettings) -> KnowledgeApplicationService:
+    redis_client = try_build_redis_client(settings.redis_url)
+    return KnowledgeApplicationService(
+        settings=settings,
+        base_repository=KnowledgeBaseRepository(db),
+        document_repository=KnowledgeDocumentRepository(db),
+        chunk_repository=KnowledgeChunkRepository(db),
+        redis_client=redis_client,
+        task_queue_publisher=(
+            TaskQueuePublisher(client=redis_client) if redis_client is not None else None
+        ))

+ 25 - 1
services/knowledge-service/app/bootstrap/app.py

@@ -1,3 +1,5 @@
+from contextlib import asynccontextmanager
+
 from core_shared.observability import add_observability
 from core_shared.security import add_internal_service_auth
 from fastapi import FastAPI
@@ -5,11 +7,33 @@ from fastapi import FastAPI
 from app.api.routes import router
 from app.bootstrap.settings import KnowledgeServiceSettings
 from app.db.session import build_session_factory
+from app.worker import BackgroundKnowledgeWorker, build_worker_key
+
+
+@asynccontextmanager
+async def lifespan(app: FastAPI):
+    settings: KnowledgeServiceSettings = app.state.settings
+    worker: BackgroundKnowledgeWorker | None = None
+    if settings.auto_worker_enabled:
+        worker = BackgroundKnowledgeWorker(
+            settings=settings,
+            session_factory=app.state.session_factory,
+            worker_key=f"api-{build_worker_key()}")
+        app.state.background_knowledge_worker = worker
+        worker.start()
+    try:
+        yield
+    finally:
+        if worker is not None:
+            worker.stop(timeout_seconds=settings.auto_worker_stop_timeout_seconds)
 
 
 def create_app() -> FastAPI:
     settings = KnowledgeServiceSettings()
-    app = FastAPI(title="agent-platform knowledge-service", version="0.1.0")
+    app = FastAPI(
+        title="agent-platform knowledge-service",
+        version="0.1.0",
+        lifespan=lifespan)
     app.state.settings = settings
     app.state.session_factory = build_session_factory(settings)
     add_observability(app, settings.service_name)

+ 14 - 1
services/knowledge-service/app/bootstrap/settings.py

@@ -4,7 +4,6 @@ from core_shared import ServiceSettings
 class KnowledgeServiceSettings(ServiceSettings):
     service_name: str = "knowledge-service"
     service_port: int = 8012
-    database_url: str = "sqlite:///./knowledge_service.db"
     default_chunk_size: int = 800
     default_chunk_overlap: int = 120
     embedding_dimensions: int = 32
@@ -19,3 +18,17 @@ class KnowledgeServiceSettings(ServiceSettings):
     retrieval_rerank_weight: float = 0.15
     retrieval_rerank_enabled: bool = True
     retrieval_candidate_multiplier: int = 5
+    object_storage_backend: str = "minio"
+    object_storage_bucket: str = "agent-platform-knowledge"
+    object_storage_endpoint_url: str = "http://127.0.0.1:9000"
+    object_storage_access_key: str = "minioadmin"
+    object_storage_secret_key: str = "minioadmin"
+    object_storage_region: str = "us-east-1"
+    object_storage_path_style: bool = True
+    async_indexing_enabled: bool = True
+    worker_poll_interval_seconds: float = 1.0
+    worker_lease_seconds: int = 300
+    worker_stale_indexing_seconds: int = 600
+    worker_max_idle_cycles: int | None = None
+    auto_worker_enabled: bool = True
+    auto_worker_stop_timeout_seconds: float = 5.0

+ 3 - 3
services/knowledge-service/app/db/models/knowledge_base.py

@@ -1,11 +1,11 @@
-from core_db import AuditMixin, Base, EntityMixin, VersionMixin
+from core_db import AuditMixin, Base, EntityMixin
 from core_shared import JSONValue
 from sqlalchemy import String, Text
-from sqlalchemy.dialects.sqlite import JSON
+from sqlalchemy import JSON
 from sqlalchemy.orm import Mapped, mapped_column
 
 
-class KnowledgeBase(EntityMixin, AuditMixin, VersionMixin, Base):
+class KnowledgeBase(EntityMixin, AuditMixin, Base):
     __tablename__ = "knowledge_base"
 
     code: Mapped[str] = mapped_column(String(64), index=True)

+ 20 - 5
services/knowledge-service/app/db/models/knowledge_chunk.py

@@ -1,11 +1,26 @@
-from core_db import AuditMixin, Base, EntityMixin, VersionMixin
+from core_db import AuditMixin, Base, EntityMixin
 from core_shared import JSONValue
-from sqlalchemy import Integer, String, Text
-from sqlalchemy.dialects.sqlite import JSON
+from sqlalchemy import Integer, String, Text, cast
+from sqlalchemy import JSON
 from sqlalchemy.orm import Mapped, mapped_column
+from sqlalchemy.sql.expression import ColumnElement
+from sqlalchemy.types import UserDefinedType
 
 
-class KnowledgeChunk(EntityMixin, AuditMixin, VersionMixin, Base):
+class PgVector(UserDefinedType[str]):
+    cache_ok = True
+
+    def __init__(self, dimensions: int) -> None:
+        self.dimensions = dimensions
+
+    def get_col_spec(self, **kw: object) -> str:
+        return f"public.vector({self.dimensions})"
+
+    def bind_expression(self, bindvalue: ColumnElement[str]) -> ColumnElement[str]:
+        return cast(bindvalue, self)
+
+
+class KnowledgeChunk(EntityMixin, AuditMixin, Base):
     __tablename__ = "knowledge_chunk"
 
     knowledge_base_id: Mapped[str] = mapped_column(String(36), index=True)
@@ -15,5 +30,5 @@ class KnowledgeChunk(EntityMixin, AuditMixin, VersionMixin, Base):
     token_count: Mapped[int] = mapped_column(Integer, default=0)
     embedding_model: Mapped[str | None] = mapped_column(String(64), nullable=True, index=True)
     embedding_json: Mapped[list[float] | None] = mapped_column(JSON, nullable=True)
-    embedding_vector: Mapped[str | None] = mapped_column(Text, nullable=True)
+    embedding_vector: Mapped[str | None] = mapped_column(PgVector(32), nullable=True)
     metadata_json: Mapped[dict[str, JSONValue] | None] = mapped_column(JSON, nullable=True)

+ 3 - 3
services/knowledge-service/app/db/models/knowledge_document.py

@@ -1,13 +1,13 @@
 from datetime import datetime
 
-from core_db import AuditMixin, Base, EntityMixin, VersionMixin
+from core_db import AuditMixin, Base, EntityMixin
 from core_shared import JSONValue
 from sqlalchemy import DateTime, String, Text
-from sqlalchemy.dialects.sqlite import JSON
+from sqlalchemy import JSON
 from sqlalchemy.orm import Mapped, mapped_column
 
 
-class KnowledgeDocument(EntityMixin, AuditMixin, VersionMixin, Base):
+class KnowledgeDocument(EntityMixin, AuditMixin, Base):
     __tablename__ = "knowledge_document"
 
     knowledge_base_id: Mapped[str] = mapped_column(String(36), index=True)

+ 183 - 12
services/knowledge-service/app/domain/repositories.py

@@ -1,6 +1,6 @@
 from datetime import datetime
 
-from sqlalchemy import delete, select, text
+from sqlalchemy import delete, or_, select, text
 from sqlalchemy.orm import Session
 
 from core_domain import KnowledgeBaseStatus, KnowledgeDocumentStatus
@@ -37,6 +37,23 @@ class KnowledgeBaseRepository:
         )
         return list(self.db.scalars(stmt))
 
+    def list_filtered(
+        self,
+        *,
+        keyword: str | None = None,
+        status: KnowledgeBaseStatus | None = None) -> list[KnowledgeBase]:
+        stmt = select(KnowledgeBase)
+        if status is not None:
+            stmt = stmt.where(KnowledgeBase.status == status)
+        if keyword:
+            pattern = f"%{keyword.strip()}%"
+            stmt = stmt.where(
+                or_(
+                    KnowledgeBase.name.ilike(pattern),
+                    KnowledgeBase.description.ilike(pattern)))
+        stmt = stmt.order_by(KnowledgeBase.created_time.desc())
+        return list(self.db.scalars(stmt))
+
     def get_by_id(self, *, knowledge_base_id: str) -> KnowledgeBase | None:
         stmt = (
             select(KnowledgeBase)
@@ -57,6 +74,37 @@ class KnowledgeBaseRepository:
         self.db.refresh(entity)
         return entity
 
+    def update(
+        self,
+        *,
+        knowledge_base_id: str,
+        name: str | None = None,
+        description: str | None = None,
+        status: KnowledgeBaseStatus | None = None,
+        metadata_json: dict[str, JSONValue] | None = None) -> KnowledgeBase | None:
+        entity = self.get_by_id(knowledge_base_id=knowledge_base_id)
+        if entity is None:
+            return None
+        if name is not None:
+            entity.name = name
+        if description is not None:
+            entity.description = description
+        if status is not None:
+            entity.status = status
+        if metadata_json is not None:
+            entity.metadata_json = metadata_json
+        self.db.commit()
+        self.db.refresh(entity)
+        return entity
+
+    def delete(self, *, knowledge_base_id: str) -> bool:
+        entity = self.get_by_id(knowledge_base_id=knowledge_base_id)
+        if entity is None:
+            return False
+        self.db.delete(entity)
+        self.db.commit()
+        return True
+
 
 class KnowledgeDocumentRepository:
     def __init__(self, db: Session) -> None:
@@ -71,7 +119,8 @@ class KnowledgeDocumentRepository:
         source_uri: str | None,
         content_text: str,
         content_hash: str | None,
-        metadata_json: dict[str, JSONValue] | None) -> KnowledgeDocument:
+        metadata_json: dict[str, JSONValue] | None,
+        status: KnowledgeDocumentStatus = "draft") -> KnowledgeDocument:
         entity = KnowledgeDocument(
             knowledge_base_id=knowledge_base_id,
             title=title,
@@ -80,7 +129,7 @@ class KnowledgeDocumentRepository:
             content_text=content_text,
             content_hash=content_hash,
             metadata_json=metadata_json,
-            status="draft")
+            status=status)
         self.db.add(entity)
         self.db.commit()
         self.db.refresh(entity)
@@ -97,6 +146,45 @@ class KnowledgeDocumentRepository:
         )
         return list(self.db.scalars(stmt))
 
+    def list_filtered(
+        self,
+        *,
+        knowledge_base_id: str | None = None,
+        keyword: str | None = None,
+        status: KnowledgeDocumentStatus | None = None,
+        source_type: str | None = None) -> list[KnowledgeDocument]:
+        stmt = select(KnowledgeDocument)
+        if knowledge_base_id is not None:
+            stmt = stmt.where(KnowledgeDocument.knowledge_base_id == knowledge_base_id)
+        if status is not None:
+            stmt = stmt.where(KnowledgeDocument.status == status)
+        if source_type is not None:
+            stmt = stmt.where(KnowledgeDocument.source_type == source_type)
+        if keyword:
+            pattern = f"%{keyword.strip()}%"
+            stmt = stmt.where(
+                or_(
+                    KnowledgeDocument.title.ilike(pattern),
+                    KnowledgeDocument.source_uri.ilike(pattern)))
+        stmt = stmt.order_by(KnowledgeDocument.created_time.desc())
+        return list(self.db.scalars(stmt))
+
+    def get_next_pending_indexing(
+        self,
+        *,
+        stale_before: datetime) -> KnowledgeDocument | None:
+        stmt = (
+            select(KnowledgeDocument)
+            .where(
+                or_(
+                    KnowledgeDocument.status == "queued",
+                    (KnowledgeDocument.status == "indexing")
+                    & (KnowledgeDocument.updated_time < stale_before)))
+            .order_by(KnowledgeDocument.updated_time.asc())
+            .limit(1)
+        )
+        return self.db.scalar(stmt)
+
     def get_by_id(self, *, document_id: str) -> KnowledgeDocument | None:
         stmt = (
             select(KnowledgeDocument)
@@ -118,6 +206,38 @@ class KnowledgeDocumentRepository:
         self.db.refresh(entity)
         return entity
 
+    def update(
+        self,
+        *,
+        document_id: str,
+        title: str | None = None,
+        source_uri: str | None = None,
+        status: KnowledgeDocumentStatus | None = None,
+        metadata_json: dict[str, JSONValue] | None = None) -> KnowledgeDocument | None:
+        entity = self.get_by_id(document_id=document_id)
+        if entity is None:
+            return None
+        if title is not None:
+            entity.title = title
+        if source_uri is not None:
+            entity.source_uri = source_uri
+        if status is not None:
+            entity.status = status
+            entity.indexed_time = datetime.utcnow() if status == "indexed" else entity.indexed_time
+        if metadata_json is not None:
+            entity.metadata_json = metadata_json
+        self.db.commit()
+        self.db.refresh(entity)
+        return entity
+
+    def delete(self, *, document_id: str) -> KnowledgeDocument | None:
+        entity = self.get_by_id(document_id=document_id)
+        if entity is None:
+            return None
+        self.db.delete(entity)
+        self.db.commit()
+        return entity
+
 
 class KnowledgeChunkRepository:
     def __init__(self, db: Session) -> None:
@@ -163,6 +283,51 @@ class KnowledgeChunkRepository:
         )
         return list(self.db.scalars(stmt))
 
+    def list_filtered(
+        self,
+        *,
+        knowledge_base_id: str | None = None,
+        document_id: str | None = None,
+        keyword: str | None = None) -> list[KnowledgeChunk]:
+        stmt = select(KnowledgeChunk)
+        if knowledge_base_id is not None:
+            stmt = stmt.where(KnowledgeChunk.knowledge_base_id == knowledge_base_id)
+        if document_id is not None:
+            stmt = stmt.where(KnowledgeChunk.document_id == document_id)
+        if keyword:
+            stmt = stmt.where(KnowledgeChunk.content_text.ilike(f"%{keyword.strip()}%"))
+        stmt = stmt.order_by(KnowledgeChunk.created_time.asc())
+        return list(self.db.scalars(stmt))
+
+    def list_by_document(self, *, document_id: str) -> list[KnowledgeChunk]:
+        return self.list_filtered(document_id=document_id)
+
+    def get_by_id(self, *, chunk_id: str) -> KnowledgeChunk | None:
+        stmt = select(KnowledgeChunk).where(KnowledgeChunk.id == chunk_id)
+        return self.db.scalar(stmt)
+
+    def delete_by_document(self, *, document_id: str) -> int:
+        result = self.db.execute(
+            delete(KnowledgeChunk)
+            .where(KnowledgeChunk.document_id == document_id))
+        self.db.commit()
+        return int(result.rowcount or 0)
+
+    def delete_by_base(self, *, knowledge_base_id: str) -> int:
+        result = self.db.execute(
+            delete(KnowledgeChunk)
+            .where(KnowledgeChunk.knowledge_base_id == knowledge_base_id))
+        self.db.commit()
+        return int(result.rowcount or 0)
+
+    def delete(self, *, chunk_id: str) -> bool:
+        entity = self.get_by_id(chunk_id=chunk_id)
+        if entity is None:
+            return False
+        self.db.delete(entity)
+        self.db.commit()
+        return True
+
     def search_by_vector(
         self,
         *,
@@ -176,21 +341,27 @@ class KnowledgeChunkRepository:
             return []
         stmt = text(
             """
-            SELECT id, 1 - (embedding_vector <=> CAST(:embedding AS vector)) AS score
+            SELECT id, 1 - (
+                embedding_vector OPERATOR(public.<=>) CAST(:embedding AS public.vector)
+            ) AS score
             FROM knowledge_chunk
             WHERE knowledge_base_id = :knowledge_base_id
               AND embedding_vector IS NOT NULL
-            ORDER BY embedding_vector <=> CAST(:embedding AS vector)
+            ORDER BY embedding_vector OPERATOR(public.<=>) CAST(:embedding AS public.vector)
             LIMIT :limit
             """
         )
-        rows = self.db.execute(
-            stmt,
-            {
-                "knowledge_base_id": knowledge_base_id,
-                "embedding": vector,
-                "limit": limit,
-            }).all()
+        try:
+            rows = self.db.execute(
+                stmt,
+                {
+                    "knowledge_base_id": knowledge_base_id,
+                    "embedding": vector,
+                    "limit": limit,
+                }).all()
+        except Exception:
+            self.db.rollback()
+            return []
         if not rows:
             return []
         chunk_ids = [str(row[0]) for row in rows]

+ 1 - 0
services/knowledge-service/app/infrastructure/__init__.py

@@ -0,0 +1 @@
+

+ 298 - 0
services/knowledge-service/app/infrastructure/object_storage.py

@@ -0,0 +1,298 @@
+from __future__ import annotations
+
+import hashlib
+from dataclasses import dataclass
+from typing import Protocol
+from uuid import uuid4
+
+from app.bootstrap.settings import KnowledgeServiceSettings
+
+
+@dataclass(frozen=True, slots=True)
+class StoredObject:
+    backend: str
+    bucket: str
+    object_key: str
+    content_type: str
+    size_bytes: int
+    etag: str | None = None
+
+    def to_metadata(self) -> dict[str, str | int | None]:
+        return {
+            "backend": self.backend,
+            "bucket": self.bucket,
+            "objectKey": self.object_key,
+            "contentType": self.content_type,
+            "sizeBytes": self.size_bytes,
+            "etag": self.etag,
+        }
+
+
+@dataclass(frozen=True, slots=True)
+class ObjectStorageStatus:
+    backend: str
+    bucket: str
+    object_key: str
+    exists: bool
+    content_type: str | None = None
+    size_bytes: int | None = None
+    etag: str | None = None
+    error_message: str | None = None
+
+
+class ObjectStorageError(RuntimeError):
+    pass
+
+
+class ObjectStorageConfigurationError(ObjectStorageError):
+    pass
+
+
+class ObjectStorageNotFoundError(ObjectStorageError, FileNotFoundError):
+    pass
+
+
+class KnowledgeObjectStorage(Protocol):
+    def put_bytes(
+        self,
+        *,
+        object_key: str,
+        content: bytes,
+        content_type: str) -> StoredObject:
+        ...
+
+    def get_bytes(self, *, object_key: str) -> bytes:
+        ...
+
+    def head_object(self, *, object_key: str) -> ObjectStorageStatus:
+        ...
+
+    def delete_object(self, *, object_key: str) -> bool:
+        ...
+
+    def health_check(self) -> dict[str, str | bool | None]:
+        ...
+
+
+class InMemoryObjectStorage:
+    _buckets: dict[str, dict[str, tuple[bytes, str, str]]] = {}
+
+    def __init__(self, *, bucket: str = "memory-knowledge") -> None:
+        self.bucket = bucket
+        self._objects = self._buckets.setdefault(bucket, {})
+
+    def put_bytes(
+        self,
+        *,
+        object_key: str,
+        content: bytes,
+        content_type: str) -> StoredObject:
+        etag = hashlib.sha256(content).hexdigest()
+        self._objects[object_key] = (content, content_type, etag)
+        return StoredObject(
+            backend="memory",
+            bucket=self.bucket,
+            object_key=object_key,
+            content_type=content_type,
+            size_bytes=len(content),
+            etag=etag)
+
+    def get_bytes(self, *, object_key: str) -> bytes:
+        try:
+            return self._objects[object_key][0]
+        except KeyError as exc:
+            raise ObjectStorageNotFoundError(f"knowledge object not found: {object_key}") from exc
+
+    def head_object(self, *, object_key: str) -> ObjectStorageStatus:
+        stored = self._objects.get(object_key)
+        if stored is None:
+            return ObjectStorageStatus(
+                backend="memory",
+                bucket=self.bucket,
+                object_key=object_key,
+                exists=False,
+                error_message=f"knowledge object not found: {object_key}")
+        content, content_type, etag = stored
+        return ObjectStorageStatus(
+            backend="memory",
+            bucket=self.bucket,
+            object_key=object_key,
+            exists=True,
+            content_type=content_type,
+            size_bytes=len(content),
+            etag=etag)
+
+    def delete_object(self, *, object_key: str) -> bool:
+        return self._objects.pop(object_key, None) is not None
+
+    def health_check(self) -> dict[str, str | bool | None]:
+        return {
+            "backend": "memory",
+            "bucket": self.bucket,
+            "available": True,
+            "message": None,
+        }
+
+
+class MinioObjectStorage:
+    def __init__(self, *, settings: KnowledgeServiceSettings) -> None:
+        try:
+            import boto3
+            from botocore.config import Config
+            from botocore.exceptions import ClientError
+        except Exception as exc:
+            raise ObjectStorageConfigurationError(
+                "boto3 is required for MinIO object storage. "
+                "Install knowledge-service dependencies before starting the service."
+            ) from exc
+
+        self._client_error_type = ClientError
+        self.bucket = settings.object_storage_bucket.strip()
+        if not self.bucket:
+            raise ObjectStorageConfigurationError("MinIO bucket is required")
+        config = Config(
+            signature_version="s3v4",
+            s3={"addressing_style": "path" if settings.object_storage_path_style else "auto"},
+        )
+        try:
+            self._client = boto3.client(
+                "s3",
+                endpoint_url=settings.object_storage_endpoint_url,
+                aws_access_key_id=settings.object_storage_access_key,
+                aws_secret_access_key=settings.object_storage_secret_key,
+                region_name=settings.object_storage_region,
+                config=config,
+            )
+        except Exception as exc:
+            raise ObjectStorageConfigurationError("failed to configure MinIO client") from exc
+        self._bucket_checked = False
+
+    def put_bytes(
+        self,
+        *,
+        object_key: str,
+        content: bytes,
+        content_type: str) -> StoredObject:
+        try:
+            self._ensure_bucket()
+            response = self._client.put_object(
+                Bucket=self.bucket,
+                Key=object_key,
+                Body=content,
+                ContentType=content_type,
+            )
+        except self._client_error_type as exc:
+            raise ObjectStorageError(f"failed to write knowledge object: {object_key}") from exc
+        etag = response.get("ETag")
+        return StoredObject(
+            backend="minio",
+            bucket=self.bucket,
+            object_key=object_key,
+            content_type=content_type,
+            size_bytes=len(content),
+            etag=str(etag).strip('"') if etag else None)
+
+    def get_bytes(self, *, object_key: str) -> bytes:
+        try:
+            response = self._client.get_object(Bucket=self.bucket, Key=object_key)
+        except self._client_error_type as exc:
+            if self._is_not_found(exc):
+                raise ObjectStorageNotFoundError(f"knowledge object not found: {object_key}") from exc
+            raise ObjectStorageError(f"failed to read knowledge object: {object_key}") from exc
+        try:
+            return response["Body"].read()
+        finally:
+            response["Body"].close()
+
+    def head_object(self, *, object_key: str) -> ObjectStorageStatus:
+        try:
+            response = self._client.head_object(Bucket=self.bucket, Key=object_key)
+        except self._client_error_type as exc:
+            if self._is_not_found(exc):
+                return ObjectStorageStatus(
+                    backend="minio",
+                    bucket=self.bucket,
+                    object_key=object_key,
+                    exists=False,
+                    error_message=f"knowledge object not found: {object_key}")
+            raise ObjectStorageError(f"failed to stat knowledge object: {object_key}") from exc
+        return ObjectStorageStatus(
+            backend="minio",
+            bucket=self.bucket,
+            object_key=object_key,
+            exists=True,
+            content_type=response.get("ContentType"),
+            size_bytes=response.get("ContentLength"),
+            etag=str(response.get("ETag")).strip('"') if response.get("ETag") else None)
+
+    def delete_object(self, *, object_key: str) -> bool:
+        try:
+            existed = self.head_object(object_key=object_key).exists
+            self._client.delete_object(Bucket=self.bucket, Key=object_key)
+            return existed
+        except ObjectStorageError:
+            raise
+        except self._client_error_type as exc:
+            raise ObjectStorageError(f"failed to delete knowledge object: {object_key}") from exc
+
+    def health_check(self) -> dict[str, str | bool | None]:
+        try:
+            self._ensure_bucket()
+        except ObjectStorageError as exc:
+            return {
+                "backend": "minio",
+                "bucket": self.bucket,
+                "available": False,
+                "message": str(exc),
+            }
+        return {
+            "backend": "minio",
+            "bucket": self.bucket,
+            "available": True,
+            "message": None,
+        }
+
+    def _ensure_bucket(self) -> None:
+        if self._bucket_checked:
+            return
+        try:
+            self._client.head_bucket(Bucket=self.bucket)
+        except self._client_error_type as exc:
+            if not self._is_not_found(exc):
+                raise ObjectStorageError(f"failed to access MinIO bucket: {self.bucket}") from exc
+            try:
+                self._client.create_bucket(Bucket=self.bucket)
+            except self._client_error_type as create_exc:
+                raise ObjectStorageError(f"failed to create MinIO bucket: {self.bucket}") from create_exc
+        self._bucket_checked = True
+
+    def _is_not_found(self, exc: Exception) -> bool:
+        response = getattr(exc, "response", None)
+        if not isinstance(response, dict):
+            return False
+        error = response.get("Error")
+        status = response.get("ResponseMetadata", {}).get("HTTPStatusCode")
+        code = error.get("Code") if isinstance(error, dict) else None
+        return status == 404 or code in {"404", "NoSuchBucket", "NoSuchKey", "NotFound"}
+
+
+def build_object_storage(settings: KnowledgeServiceSettings) -> KnowledgeObjectStorage:
+    backend = settings.object_storage_backend.strip().lower()
+    if backend == "memory":
+        return InMemoryObjectStorage(bucket=settings.object_storage_bucket)
+    if backend == "minio":
+        return MinioObjectStorage(settings=settings)
+    raise ValueError(f"unsupported knowledge object storage backend: {settings.object_storage_backend}")
+
+
+def build_document_object_key(
+    *,
+    knowledge_base_id: str,
+    source_type: str,
+    title: str) -> str:
+    safe_title = "".join(
+        char.lower() if char.isalnum() else "-"
+        for char in title
+    ).strip("-") or "document"
+    suffix = source_type.strip().lower().removeprefix(".") or "txt"
+    return f"knowledge/{knowledge_base_id}/documents/{uuid4().hex}/{safe_title[:80]}.{suffix}"

+ 377 - 1
services/knowledge-service/app/schemas/knowledge.py

@@ -1,10 +1,12 @@
-from typing import TYPE_CHECKING
+from datetime import datetime
+from typing import TYPE_CHECKING, Generic, Literal, TypeVar
 
 from core_domain import (
     KnowledgeBaseContract,
     KnowledgeBaseStatus,
     KnowledgeChunkContract,
     KnowledgeDocumentContract,
+    KnowledgeDocumentStatus,
     KnowledgeSearchRequestContract,
     KnowledgeSearchResultContract,
 )
@@ -14,6 +16,55 @@ from pydantic import BaseModel, Field
 if TYPE_CHECKING:
     from app.db.models import KnowledgeBase, KnowledgeChunk, KnowledgeDocument
 
+T = TypeVar("T")
+
+
+class ApiErrorResponse(BaseModel):
+    errorType: str
+    message: str
+    details: dict[str, JSONValue] = Field(default_factory=dict)
+
+
+class ApiResponse(BaseModel, Generic[T]):
+    success: bool = True
+    data: T | None = None
+    error: ApiErrorResponse | None = None
+    requestId: str
+    serverTime: datetime
+
+
+class PageRequest(BaseModel):
+    page: int = Field(default=1, ge=1)
+    pageSize: int = Field(default=20, ge=1, le=200)
+    keyword: str | None = None
+
+    @property
+    def offset(self) -> int:
+        return (self.page - 1) * self.pageSize
+
+
+class PageResult(BaseModel, Generic[T]):
+    items: list[T]
+    total: int
+    page: int
+    pageSize: int
+    hasMore: bool
+
+    @classmethod
+    def from_items(
+        cls,
+        *,
+        items: list[T],
+        total: int,
+        page: int,
+        page_size: int) -> "PageResult[T]":
+        return cls(
+            items=items,
+            total=total,
+            page=page,
+            pageSize=page_size,
+            hasMore=page * page_size < total)
+
 
 class KnowledgeBaseCreateRequest(BaseModel):
     code: str
@@ -80,3 +131,328 @@ class KnowledgeSearchRequest(KnowledgeSearchRequestContract):
 
 class KnowledgeSearchResultResponse(KnowledgeSearchResultContract):
     pass
+
+
+class KnowledgeBaseDto(BaseModel):
+    id: str
+    name: str
+    description: str | None = None
+    status: KnowledgeBaseStatus
+    metadata: dict[str, JSONValue] | None = None
+    createdTime: datetime
+
+    @classmethod
+    def from_entity(cls, entity: "KnowledgeBase") -> "KnowledgeBaseDto":
+        return cls(
+            id=entity.id,
+            name=entity.name,
+            description=entity.description,
+            status=entity.status,
+            metadata=entity.metadata_json,
+            createdTime=entity.created_time)
+
+
+class KnowledgeBaseListRequestDto(PageRequest):
+    status: KnowledgeBaseStatus | None = None
+
+
+class KnowledgeBaseCreateRequestDto(BaseModel):
+    name: str
+    description: str | None = None
+    metadata: dict[str, JSONValue] = Field(default_factory=dict)
+
+
+class KnowledgeBaseDetailRequestDto(BaseModel):
+    knowledgeBaseId: str
+
+
+class KnowledgeBaseUpdateRequestDto(BaseModel):
+    knowledgeBaseId: str
+    name: str | None = None
+    description: str | None = None
+    status: KnowledgeBaseStatus | None = None
+    metadata: dict[str, JSONValue] | None = None
+
+
+class KnowledgeBaseStatusRequestDto(BaseModel):
+    knowledgeBaseId: str
+    status: KnowledgeBaseStatus
+
+
+class KnowledgeBaseDeleteRequestDto(BaseModel):
+    knowledgeBaseId: str
+
+
+class KnowledgeDocumentDto(BaseModel):
+    id: str
+    knowledgeBaseId: str
+    title: str
+    sourceType: str
+    sourceUri: str | None = None
+    status: KnowledgeDocumentStatus
+    contentHash: str | None = None
+    objectStorage: dict[str, JSONValue] | None = None
+    metadata: dict[str, JSONValue] | None = None
+    indexedTime: datetime | None = None
+    createdTime: datetime
+
+    @classmethod
+    def from_entity(cls, entity: "KnowledgeDocument") -> "KnowledgeDocumentDto":
+        metadata = entity.metadata_json or {}
+        object_storage = metadata.get("object_storage")
+        return cls(
+            id=entity.id,
+            knowledgeBaseId=entity.knowledge_base_id,
+            title=entity.title,
+            sourceType=entity.source_type,
+            sourceUri=entity.source_uri,
+            status=entity.status,
+            contentHash=entity.content_hash,
+            objectStorage=object_storage if isinstance(object_storage, dict) else None,
+            metadata=entity.metadata_json,
+            indexedTime=entity.indexed_time,
+            createdTime=entity.created_time)
+
+
+class KnowledgeDocumentListRequestDto(PageRequest):
+    knowledgeBaseId: str | None = None
+    status: KnowledgeDocumentStatus | None = None
+    sourceType: str | None = None
+
+
+class KnowledgeDocumentCreateRequestDto(BaseModel):
+    knowledgeBaseId: str
+    title: str
+    contentText: str | None = None
+    contentBase64: str | None = None
+    sourceType: str = "text"
+    sourceUri: str | None = None
+    metadata: dict[str, JSONValue] = Field(default_factory=dict)
+    chunkSize: int | None = Field(default=None, gt=0)
+    chunkOverlap: int | None = Field(default=None, ge=0)
+    asyncMode: bool | None = None
+
+
+class KnowledgeDocumentDetailRequestDto(BaseModel):
+    documentId: str
+
+
+class KnowledgeDocumentUpdateRequestDto(BaseModel):
+    documentId: str
+    title: str | None = None
+    sourceUri: str | None = None
+    status: KnowledgeDocumentStatus | None = None
+    metadata: dict[str, JSONValue] | None = None
+
+
+class KnowledgeDocumentStatusRequestDto(BaseModel):
+    documentId: str
+    status: KnowledgeDocumentStatus
+
+
+class KnowledgeDocumentDeleteRequestDto(BaseModel):
+    documentId: str
+
+
+class KnowledgeDocumentReindexRequestDto(BaseModel):
+    documentId: str
+    chunkSize: int | None = Field(default=None, gt=0)
+    chunkOverlap: int | None = Field(default=None, ge=0)
+    asyncMode: bool | None = None
+
+
+class KnowledgeDocumentContentRequestDto(BaseModel):
+    documentId: str
+    includeText: bool = True
+    includeBase64: bool = False
+
+
+class KnowledgeDocumentContentData(BaseModel):
+    documentId: str
+    title: str
+    sourceType: str
+    contentType: str | None = None
+    sizeBytes: int
+    contentText: str | None = None
+    contentBase64: str | None = None
+    objectStorage: dict[str, JSONValue] | None = None
+
+
+class KnowledgeDocumentStorageStatusRequestDto(BaseModel):
+    documentId: str
+
+
+class KnowledgeDocumentStorageStatusData(BaseModel):
+    documentId: str
+    exists: bool
+    objectStorage: dict[str, JSONValue] | None = None
+    contentType: str | None = None
+    sizeBytes: int | None = None
+    etag: str | None = None
+    errorMessage: str | None = None
+    checkedTime: datetime
+
+
+class KnowledgeDocumentParseRequestDto(BaseModel):
+    sourceType: str = "auto"
+    sourceUri: str | None = None
+    contentText: str | None = None
+    contentBase64: str | None = None
+
+
+class KnowledgeDocumentParseData(BaseModel):
+    contentText: str
+    sourceType: str
+    metadata: dict[str, JSONValue] = Field(default_factory=dict)
+
+
+class KnowledgeChunkDto(BaseModel):
+    id: str
+    knowledgeBaseId: str
+    documentId: str
+    chunkIndex: int
+    contentText: str
+    tokenCount: int
+    embeddingModel: str | None = None
+    embedding: list[float] | None = None
+    metadata: dict[str, JSONValue] | None = None
+    createdTime: datetime
+
+    @classmethod
+    def from_entity(cls, entity: "KnowledgeChunk") -> "KnowledgeChunkDto":
+        return cls(
+            id=entity.id,
+            knowledgeBaseId=entity.knowledge_base_id,
+            documentId=entity.document_id,
+            chunkIndex=entity.chunk_index,
+            contentText=entity.content_text,
+            tokenCount=entity.token_count,
+            embeddingModel=entity.embedding_model,
+            embedding=entity.embedding_json,
+            metadata=entity.metadata_json,
+            createdTime=entity.created_time)
+
+
+class KnowledgeChunkListRequestDto(PageRequest):
+    knowledgeBaseId: str | None = None
+    documentId: str | None = None
+
+
+class KnowledgeChunkDetailRequestDto(BaseModel):
+    chunkId: str
+
+
+class KnowledgeChunkDeleteRequestDto(BaseModel):
+    chunkId: str
+
+
+class KnowledgeSearchRequestDto(BaseModel):
+    knowledgeBaseId: str
+    query: str
+    topK: int = Field(default=5, ge=1, le=50)
+    filters: dict[str, JSONValue] = Field(default_factory=dict)
+
+
+class KnowledgeSearchResultDto(BaseModel):
+    chunk: KnowledgeChunkDto
+    document: KnowledgeDocumentDto
+    score: float
+    scoreDetails: dict[str, JSONValue] = Field(default_factory=dict)
+
+
+class KnowledgeDocumentIngestData(BaseModel):
+    document: KnowledgeDocumentDto
+    chunks: list[KnowledgeChunkDto]
+    queued: bool = False
+    job: "KnowledgeIndexJobData | None" = None
+
+
+KnowledgeIndexJobStatus = Literal["queued", "running", "completed", "failed", "skipped"]
+KnowledgeIndexJobAction = Literal["index", "reindex"]
+
+
+class KnowledgeIndexJobData(BaseModel):
+    jobId: str
+    documentId: str
+    knowledgeBaseId: str | None = None
+    documentTitle: str | None = None
+    action: KnowledgeIndexJobAction
+    status: KnowledgeIndexJobStatus
+    progress: int = Field(default=0, ge=0, le=100)
+    queueName: str | None = None
+    workerKey: str | None = None
+    errorMessage: str | None = None
+    chunkSize: int | None = None
+    chunkOverlap: int | None = None
+    queuedTime: datetime | None = None
+    startedTime: datetime | None = None
+    completedTime: datetime | None = None
+
+
+class KnowledgeIndexJobListRequestDto(PageRequest):
+    knowledgeBaseId: str | None = None
+    documentId: str | None = None
+    status: KnowledgeIndexJobStatus | None = None
+
+
+class KnowledgeIndexJobDetailRequestDto(BaseModel):
+    documentId: str
+
+
+class KnowledgeIndexJobRetryRequestDto(BaseModel):
+    documentId: str
+    chunkSize: int | None = Field(default=None, gt=0)
+    chunkOverlap: int | None = Field(default=None, ge=0)
+
+
+class KnowledgeBaseReindexRequestDto(BaseModel):
+    knowledgeBaseId: str
+    chunkSize: int | None = Field(default=None, gt=0)
+    chunkOverlap: int | None = Field(default=None, ge=0)
+
+
+class KnowledgeBaseReindexData(BaseModel):
+    knowledgeBaseId: str
+    queuedCount: int
+    jobs: list[KnowledgeIndexJobData]
+
+
+class KnowledgeStorageHealthRequestDto(BaseModel):
+    pass
+
+
+class KnowledgeStorageHealthData(BaseModel):
+    backend: str
+    bucket: str
+    available: bool
+    message: str | None = None
+    checkedTime: datetime
+
+
+class KnowledgeSettingsDto(BaseModel):
+    knowledgeBaseId: str | None = None
+    retrievalMode: str = "hybrid"
+    embeddingModelId: str = "auto"
+    rerankModelId: str = "auto"
+    chunkSize: int = 800
+    chunkOverlap: int = 120
+    topK: int = 5
+    minScore: float = 0.0
+    maxCandidates: int = 50
+    keywordWeight: float = 0.55
+    vectorWeight: float = 0.30
+    rerankWeight: float = 0.15
+    queryRewrite: bool = False
+    requireCitations: bool = True
+
+
+class KnowledgeSettingsUpdateRequestDto(KnowledgeSettingsDto):
+    knowledgeBaseId: str | None = None
+
+
+class DeleteData(BaseModel):
+    deleted: bool
+    knowledgeBaseId: str | None = None
+    documentId: str | None = None
+    chunkId: str | None = None
+    objectDeleted: bool | None = None

+ 176 - 0
services/knowledge-service/app/worker.py

@@ -0,0 +1,176 @@
+from __future__ import annotations
+
+import os
+import socket
+import time
+import traceback
+from dataclasses import dataclass
+from math import ceil
+from threading import Event, Thread
+from uuid import uuid4
+
+from core_shared import JSONValue, try_build_redis_client
+from core_shared.task_queue import KNOWLEDGE_DOCUMENT_QUEUE, build_task_queue_consumer
+from sqlalchemy.orm import Session, sessionmaker
+
+from app.application.services import build_knowledge_application_service
+from app.bootstrap.settings import KnowledgeServiceSettings
+from app.db.session import build_session_factory
+
+
+@dataclass(frozen=True)
+class KnowledgeWorkerStats:
+    worker_key: str
+    executed_count: int = 0
+    idle_count: int = 0
+    error_count: int = 0
+
+
+class KnowledgeWorker:
+    def __init__(
+        self,
+        *,
+        settings: KnowledgeServiceSettings,
+        session_factory: sessionmaker[Session],
+        worker_key: str) -> None:
+        self.settings = settings
+        self.session_factory = session_factory
+        self.worker_key = worker_key
+        self.redis_client = try_build_redis_client(settings.redis_url)
+        self.task_queue = build_task_queue_consumer(
+            client=self.redis_client,
+            queue_name=KNOWLEDGE_DOCUMENT_QUEUE)
+
+    def run_forever(self) -> KnowledgeWorkerStats:
+        executed_count = 0
+        idle_count = 0
+        error_count = 0
+
+        while True:
+            try:
+                executed = self.run_once()
+            except Exception:
+                error_count += 1
+                traceback.print_exc()
+                executed = False
+
+            if executed:
+                executed_count += 1
+                idle_count = 0
+            else:
+                idle_count += 1
+                if self.task_queue is None:
+                    time.sleep(self.settings.worker_poll_interval_seconds)
+
+            if self.settings.worker_max_idle_cycles is not None:
+                if idle_count >= self.settings.worker_max_idle_cycles:
+                    return KnowledgeWorkerStats(
+                        worker_key=self.worker_key,
+                        executed_count=executed_count,
+                        idle_count=idle_count,
+                        error_count=error_count)
+
+    def run_once(self) -> bool:
+        queue_payload = self._wait_for_queue_signal()
+        db = self.session_factory()
+        try:
+            service = build_knowledge_application_service(db=db, settings=self.settings)
+            if queue_payload is not None:
+                document_id = _optional_str(queue_payload.get("document_id"))
+                action = _optional_str(queue_payload.get("action")) or "reindex"
+                job_id = _optional_str(queue_payload.get("job_id"))
+                if document_id is not None:
+                    result = service.execute_document_index_job(
+                        document_id=document_id,
+                        action=action,
+                        job_id=job_id,
+                        worker_key=self.worker_key,
+                        lease_seconds=self.settings.worker_lease_seconds,
+                        redis_client=self.redis_client)
+                    if result is not None:
+                        return True
+            result = service.execute_next_pending_document_job(
+                worker_key=self.worker_key,
+                lease_seconds=self.settings.worker_lease_seconds,
+                stale_indexing_seconds=self.settings.worker_stale_indexing_seconds,
+                redis_client=self.redis_client)
+            return result is not None
+        finally:
+            db.close()
+
+    def _wait_for_queue_signal(self) -> dict[str, JSONValue] | None:
+        if self.task_queue is None:
+            return None
+        try:
+            return self.task_queue.dequeue(
+                timeout_seconds=max(1, ceil(self.settings.worker_poll_interval_seconds)))
+        except Exception:
+            return None
+
+
+class BackgroundKnowledgeWorker:
+    def __init__(
+        self,
+        *,
+        settings: KnowledgeServiceSettings,
+        session_factory: sessionmaker[Session],
+        worker_key: str) -> None:
+        self.settings = settings
+        self.worker = KnowledgeWorker(
+            settings=settings,
+            session_factory=session_factory,
+            worker_key=worker_key)
+        self.stop_event = Event()
+        self.thread = Thread(
+            target=self._run,
+            name=f"knowledge-auto-worker-{worker_key}",
+            daemon=True)
+
+    def start(self) -> None:
+        self.thread.start()
+
+    def stop(self, *, timeout_seconds: float) -> None:
+        self.stop_event.set()
+        self.thread.join(timeout=timeout_seconds)
+
+    def _run(self) -> None:
+        while not self.stop_event.is_set():
+            try:
+                executed = self.worker.run_once()
+            except Exception:
+                traceback.print_exc()
+                executed = False
+            if not executed:
+                self.stop_event.wait(self.settings.worker_poll_interval_seconds)
+
+
+def _optional_str(value: JSONValue) -> str | None:
+    return value if isinstance(value, str) and value else None
+
+
+def build_worker_key() -> str:
+    configured_key = os.getenv("AGENT_PLATFORM_WORKER_KEY")
+    if configured_key:
+        return configured_key
+    hostname = socket.gethostname()
+    return f"{hostname}-{uuid4().hex[:8]}"
+
+
+def main() -> None:
+    settings = KnowledgeServiceSettings()
+    worker = KnowledgeWorker(
+        settings=settings,
+        session_factory=build_session_factory(settings),
+        worker_key=build_worker_key())
+    stats = worker.run_forever()
+    print(
+        "knowledge-worker stopped "
+        f"worker_key={stats.worker_key} "
+        f"executed_count={stats.executed_count} "
+        f"idle_count={stats.idle_count} "
+        f"error_count={stats.error_count}",
+        flush=True)
+
+
+if __name__ == "__main__":
+    main()

+ 1 - 0
services/knowledge-service/pyproject.toml

@@ -9,6 +9,7 @@ description = "Knowledge base and RAG retrieval service for agent platform."
 requires-python = ">=3.11"
 dependencies = [
   "alembic>=1.13,<2.0",
+  "boto3>=1.34,<2.0",
   "fastapi>=0.111,<1.0",
   "httpx>=0.27,<1.0",
   "uvicorn[standard]>=0.30,<1.0",

+ 1 - 1
services/memory-service/alembic.ini

@@ -1,7 +1,7 @@
 [alembic]
 script_location = alembic
 prepend_sys_path = .
-sqlalchemy.url = sqlite:///./memory_service.db
+sqlalchemy.url = postgresql+psycopg://admin:hFOvG5UBeK5KIGhz5cQH@git.newpoint.work:5432/vectordb
 
 [loggers]
 keys = root,sqlalchemy,alembic

+ 15 - 2
services/memory-service/alembic/env.py

@@ -1,10 +1,16 @@
+import os
 from logging.config import fileConfig
 
 from alembic import context
 from app.db.models import Base
 from sqlalchemy import engine_from_config, pool
 
+SERVICE_VERSION_TABLE = "memory_alembic_version"
+
 config = context.config
+database_url = os.getenv("AGENT_PLATFORM_DATABASE_URL")
+if database_url:
+    config.set_main_option("sqlalchemy.url", database_url.replace("%", "%%"))
 
 if config.config_file_name is not None:
     fileConfig(config.config_file_name)
@@ -14,7 +20,11 @@ target_metadata = Base.metadata
 
 def run_migrations_offline() -> None:
     url = config.get_main_option("sqlalchemy.url")
-    context.configure(url=url, target_metadata=target_metadata, literal_binds=True)
+    context.configure(
+        url=url,
+        target_metadata=target_metadata,
+        literal_binds=True,
+        version_table=SERVICE_VERSION_TABLE)
 
     with context.begin_transaction():
         context.run_migrations()
@@ -27,7 +37,10 @@ def run_migrations_online() -> None:
         poolclass=pool.NullPool)
 
     with connectable.connect() as connection:
-        context.configure(connection=connection, target_metadata=target_metadata)
+        context.configure(
+            connection=connection,
+            target_metadata=target_metadata,
+            version_table=SERVICE_VERSION_TABLE)
 
         with context.begin_transaction():
             context.run_migrations()

+ 22 - 0
services/memory-service/alembic/versions/20260429_9001_remove_version_columns.py

@@ -0,0 +1,22 @@
+"""Remove business version schema artifacts.
+
+Revision ID: 20260429_9001_memory
+Revises: 20260425_0002
+Create Date: 2026-04-29 00:00:00.000000
+"""
+
+from alembic import op
+
+revision: str = "20260429_9001_memory"
+down_revision: str | None = "20260425_0002"
+branch_labels = None
+depends_on = None
+
+
+def upgrade() -> None:
+    op.execute("DO $$\nDECLARE\n    table_record record;\nBEGIN\n    FOR table_record IN\n        SELECT table_name\n        FROM information_schema.columns\n        WHERE table_schema = current_schema()\n          AND column_name = 'version'\n    LOOP\n        EXECUTE format('ALTER TABLE %I DROP COLUMN IF EXISTS version', table_record.table_name);\n    END LOOP;\nEND $$;")
+
+
+def downgrade() -> None:
+    # Business version tables and columns were intentionally removed.
+    pass

+ 106 - 54
services/memory-service/app/api/routes.py

@@ -1,24 +1,48 @@
-from core_domain import MemoryScopeType, MemoryStatus, ServiceHealth
-from fastapi import APIRouter, Depends, HTTPException, Query
+from datetime import datetime
+from typing import TypeVar
+from uuid import uuid4
+
+from core_domain import ServiceHealth
+from fastapi import APIRouter, Depends, HTTPException, Request
 from sqlalchemy import text
 from sqlalchemy.orm import Session
 
-from app.application.services import MemoryApplicationService
+from app.application.services import MemoryApplicationService, build_memory_application_service
+from app.bootstrap.settings import MemoryServiceSettings
 from app.db.session import get_db
-from app.domain.repositories import MemoryItemRepository
 from app.schemas.memory import (
-    MemoryCreateRequest,
-    MemoryResponse,
-    MemorySearchRequest,
-    MemorySearchResultResponse,
-    MemoryStatusUpdateRequest,
+    ApiResponse,
+    DeleteData,
+    MemoryCreateRequestDto,
+    MemoryDeleteRequestDto,
+    MemoryDetailRequestDto,
+    MemoryItemDto,
+    MemoryListRequestDto,
+    MemorySearchRequestDto,
+    MemorySearchResultDto,
+    MemoryStatusRequestDto,
+    MemoryUpdateRequestDto,
+    PageResult,
 )
 
 router = APIRouter()
+T = TypeVar("T")
+
+
+def ok(data: T) -> ApiResponse[T]:
+    return ApiResponse(
+        success=True,
+        data=data,
+        error=None,
+        requestId=str(uuid4()),
+        serverTime=datetime.utcnow())
 
 
-def get_memory_application_service(db: Session = Depends(get_db)) -> MemoryApplicationService:
-    return MemoryApplicationService(memory_repository=MemoryItemRepository(db))
+def get_memory_application_service(
+    request: Request,
+    db: Session = Depends(get_db)) -> MemoryApplicationService:
+    settings: MemoryServiceSettings = request.app.state.settings
+    return build_memory_application_service(db=db, settings=settings)
 
 
 @router.get("/health", response_model=ServiceHealth)
@@ -27,47 +51,75 @@ def health_check(db: Session = Depends(get_db)) -> ServiceHealth:
     return ServiceHealth(service="memory-service", status="ok", database="ok")
 
 
-@router.post("", response_model=MemoryResponse)
-def create_memory(
-    payload: MemoryCreateRequest,
-    service: MemoryApplicationService = Depends(get_memory_application_service)) -> MemoryResponse:
-    entity = service.create_memory(payload)
-    return MemoryResponse.from_entity(entity)
-
-
-@router.get("", response_model=list[MemoryResponse])
-def list_memories(
-    scope_type: MemoryScopeType | None = Query(default=None),
-    scope_id: str | None = Query(default=None),
-    status: MemoryStatus | None = Query(default="active"),
-    limit: int = Query(default=100, ge=1, le=500),
-    service: MemoryApplicationService = Depends(get_memory_application_service)) -> list[MemoryResponse]:
-    return [
-        MemoryResponse.from_entity(item)
-        for item in service.list_memories(
-            scope_type=scope_type,
-            scope_id=scope_id,
-            status=status,
-            limit=limit)
-    ]
-
-
-@router.post("/search", response_model=list[MemorySearchResultResponse])
-def search_memories(
-    payload: MemorySearchRequest,
-    service: MemoryApplicationService = Depends(get_memory_application_service)) -> list[MemorySearchResultResponse]:
-    return [
-        MemorySearchResultResponse.from_entity(item, score=score, score_json=score_json)
-        for item, score, score_json in service.search_memories(payload)
-    ]
-
-
-@router.patch("/{memory_id}/status", response_model=MemoryResponse)
-def update_memory_status(
-    memory_id: str,
-    payload: MemoryStatusUpdateRequest,
-    service: MemoryApplicationService = Depends(get_memory_application_service)) -> MemoryResponse:
-    entity = service.update_memory_status(memory_id=memory_id, payload=payload)
+@router.post("/list", response_model=ApiResponse[PageResult[MemoryItemDto]])
+def list_memories_contract(
+    payload: MemoryListRequestDto,
+    service: MemoryApplicationService = Depends(get_memory_application_service)) -> ApiResponse[PageResult[MemoryItemDto]]:
+    items, total = service.list_memories_contract(payload)
+    return ok(PageResult[MemoryItemDto].from_items(
+        items=[MemoryItemDto.from_entity(item) for item in items],
+        total=total,
+        page=payload.page,
+        page_size=payload.pageSize))
+
+
+@router.post("/create", response_model=ApiResponse[MemoryItemDto])
+def create_memory_contract(
+    payload: MemoryCreateRequestDto,
+    service: MemoryApplicationService = Depends(get_memory_application_service)) -> ApiResponse[MemoryItemDto]:
+    return ok(MemoryItemDto.from_entity(service.create_memory_from_contract(payload)))
+
+
+@router.post("/detail", response_model=ApiResponse[MemoryItemDto])
+def detail_memory_contract(
+    payload: MemoryDetailRequestDto,
+    service: MemoryApplicationService = Depends(get_memory_application_service)) -> ApiResponse[MemoryItemDto]:
+    entity = service.get_memory(memory_id=payload.memoryId)
+    if entity is None:
+        raise HTTPException(status_code=404, detail=f"memory not found: {payload.memoryId}")
+    return ok(MemoryItemDto.from_entity(entity))
+
+
+@router.post("/update", response_model=ApiResponse[MemoryItemDto])
+def update_memory_contract(
+    payload: MemoryUpdateRequestDto,
+    service: MemoryApplicationService = Depends(get_memory_application_service)) -> ApiResponse[MemoryItemDto]:
+    entity = service.update_memory(payload)
     if entity is None:
-        raise HTTPException(status_code=404, detail=f"memory not found: {memory_id}")
-    return MemoryResponse.from_entity(entity)
+        raise HTTPException(status_code=404, detail=f"memory not found: {payload.memoryId}")
+    return ok(MemoryItemDto.from_entity(entity))
+
+
+@router.post("/status", response_model=ApiResponse[MemoryItemDto])
+def update_memory_status_contract(
+    payload: MemoryStatusRequestDto,
+    service: MemoryApplicationService = Depends(get_memory_application_service)) -> ApiResponse[MemoryItemDto]:
+    entity = service.update_memory_status(
+        memory_id=payload.memoryId,
+        payload=payload)
+    if entity is None:
+        raise HTTPException(status_code=404, detail=f"memory not found: {payload.memoryId}")
+    return ok(MemoryItemDto.from_entity(entity))
+
+
+@router.post("/delete", response_model=ApiResponse[DeleteData])
+def delete_memory_contract(
+    payload: MemoryDeleteRequestDto,
+    service: MemoryApplicationService = Depends(get_memory_application_service)) -> ApiResponse[DeleteData]:
+    entity = service.delete_memory(memory_id=payload.memoryId)
+    if entity is None:
+        raise HTTPException(status_code=404, detail=f"memory not found: {payload.memoryId}")
+    return ok(DeleteData(deleted=True, memoryId=payload.memoryId))
+
+
+@router.post("/search/query", response_model=ApiResponse[list[MemorySearchResultDto]])
+def search_memories_contract(
+    payload: MemorySearchRequestDto,
+    service: MemoryApplicationService = Depends(get_memory_application_service)) -> ApiResponse[list[MemorySearchResultDto]]:
+    return ok([
+        MemorySearchResultDto(
+            item=MemoryItemDto.from_entity(item),
+            score=score,
+            scoreDetails=score_json)
+        for item, score, score_json in service.search_memories_contract(payload)
+    ])

+ 245 - 4
services/memory-service/app/application/services.py

@@ -1,6 +1,15 @@
+from __future__ import annotations
+
+import hashlib
+import json
 from datetime import datetime
+from typing import TYPE_CHECKING
 
 from core_domain import MemoryScopeType, MemoryStatus
+from sqlalchemy.orm import Session
+
+from core_shared import JSONValue, try_build_redis_client
+from core_shared.task_queue import TaskQueuePublisher
 
 from app.application.retrieval import (
     build_hash_embedding,
@@ -11,7 +20,18 @@ from app.application.retrieval import (
 from app.bootstrap.settings import MemoryServiceSettings
 from app.db.models import MemoryItem
 from app.domain.repositories import MemoryItemRepository
-from app.schemas.memory import MemoryCreateRequest, MemorySearchRequest, MemoryStatusUpdateRequest
+from app.schemas.memory import (
+    MemoryCreateRequest,
+    MemoryCreateRequestDto,
+    MemoryListRequestDto,
+    MemorySearchRequest,
+    MemorySearchRequestDto,
+    MemoryStatusUpdateRequest,
+    MemoryUpdateRequestDto,
+)
+
+if TYPE_CHECKING:
+    from redis import Redis
 
 
 class MemoryApplicationService:
@@ -19,15 +39,19 @@ class MemoryApplicationService:
         self,
         *,
         memory_repository: MemoryItemRepository,
-        settings: MemoryServiceSettings | None = None) -> None:
+        settings: MemoryServiceSettings | None = None,
+        redis_client: Redis | None = None,
+        task_queue_publisher: TaskQueuePublisher | None = None) -> None:
         self.memory_repository = memory_repository
         self.settings = settings or MemoryServiceSettings()
+        self.redis_client = redis_client
+        self.task_queue_publisher = task_queue_publisher
 
     def create_memory(self, payload: MemoryCreateRequest) -> MemoryItem:
         embedding_json = build_hash_embedding(
             payload.content_text,
             dimensions=self.settings.embedding_dimensions)
-        return self.memory_repository.create(
+        entity = self.memory_repository.create(
             scope_type=payload.scope_type,
             scope_id=payload.scope_id,
             memory_type=payload.memory_type,
@@ -42,6 +66,23 @@ class MemoryApplicationService:
             source_ref=payload.source_ref,
             importance_score=payload.importance_score,
             expires_time=payload.expires_time)
+        self._bump_search_cache_generation()
+        return entity
+
+    def create_memory_from_contract(self, payload: MemoryCreateRequestDto) -> MemoryItem:
+        return self.create_memory(MemoryCreateRequest(
+            scope_type=payload.scopeType,
+            scope_id=payload.scopeId,
+            memory_type=payload.memoryType,
+            content_text=payload.contentText,
+            content_json=payload.content,
+            metadata_json=payload.metadata,
+            owner_agent_id=payload.ownerAgentId,
+            user_id=payload.userId,
+            session_id=payload.sessionId,
+            source_ref=payload.sourceRef,
+            importance_score=payload.importanceScore,
+            expires_time=payload.expiresTime))
 
     def list_memories(
         self,
@@ -56,6 +97,20 @@ class MemoryApplicationService:
             status=status,
             limit=limit)
 
+    def list_memories_contract(self, payload: MemoryListRequestDto) -> tuple[list[MemoryItem], int]:
+        return self.memory_repository.list_filtered(
+            scope_type=payload.scopeType,
+            scope_id=payload.scopeId,
+            memory_type=payload.memoryType,
+            status=payload.status,
+            owner_agent_id=payload.ownerAgentId,
+            user_id=payload.userId,
+            session_id=payload.sessionId,
+            keyword=payload.keyword,
+            include_expired=payload.includeExpired,
+            offset=payload.offset,
+            limit=payload.pageSize)
+
     def search_memories(
         self,
         payload: MemorySearchRequest) -> list[tuple[MemoryItem, float, dict[str, float | str]]]:
@@ -79,14 +134,186 @@ class MemoryApplicationService:
         self.memory_repository.touch_many(memory_ids=[item.id for item in items], accessed_time=now)
         return scored_items[: payload.limit]
 
+    def search_memories_contract(
+        self,
+        payload: MemorySearchRequestDto) -> list[tuple[MemoryItem, float, dict[str, float | str]]]:
+        cached = self._read_search_cache(payload=payload)
+        if cached is not None:
+            self._touch_memory_access([item.id for item, _, _ in cached])
+            return cached
+        query_embedding = build_hash_embedding(
+            payload.query,
+            dimensions=self.settings.embedding_dimensions)
+        candidates = self.memory_repository.search_candidates(
+            scope_type=payload.scopeType,
+            scope_id=payload.scopeId,
+            owner_agent_id=payload.ownerAgentId,
+            user_id=payload.userId,
+            session_id=payload.sessionId,
+            memory_type=payload.memoryType,
+            limit=max(payload.limit * 10, payload.limit))
+        scored_items = [
+            self._score(item=item, query=payload.query, query_embedding=query_embedding)
+            for item in candidates
+        ]
+        scored_items.sort(key=lambda item: item[1], reverse=True)
+        results = scored_items[: payload.limit]
+        self._write_search_cache(payload=payload, results=results)
+        self._touch_memory_access([item.id for item, _, _ in results])
+        return results
+
     def update_memory_status(
         self,
         *,
         memory_id: str,
         payload: MemoryStatusUpdateRequest) -> MemoryItem | None:
-        return self.memory_repository.update_status(
+        entity = self.memory_repository.update_status(
             memory_id=memory_id,
             status=payload.status)
+        if entity is not None:
+            self._bump_search_cache_generation()
+        return entity
+
+    def get_memory(self, *, memory_id: str) -> MemoryItem | None:
+        return self.memory_repository.get_by_id(memory_id=memory_id)
+
+    def update_memory(self, payload: MemoryUpdateRequestDto) -> MemoryItem | None:
+        embedding_json: list[float] | None = None
+        embedding_model: str | None = None
+        if payload.contentText is not None:
+            embedding_json = build_hash_embedding(
+                payload.contentText,
+                dimensions=self.settings.embedding_dimensions)
+            embedding_model = self.settings.embedding_model
+        entity = self.memory_repository.update(
+            memory_id=payload.memoryId,
+            scope_type=payload.scopeType,
+            scope_id=payload.scopeId,
+            memory_type=payload.memoryType,
+            content_text=payload.contentText,
+            content_json=payload.content,
+            metadata_json=payload.metadata,
+            embedding_model=embedding_model,
+            embedding_json=embedding_json,
+            owner_agent_id=payload.ownerAgentId,
+            user_id=payload.userId,
+            session_id=payload.sessionId,
+            source_ref=payload.sourceRef,
+            importance_score=payload.importanceScore,
+            expires_time=payload.expiresTime)
+        if entity is not None:
+            self._bump_search_cache_generation()
+        return entity
+
+    def delete_memory(self, *, memory_id: str) -> MemoryItem | None:
+        entity = self.memory_repository.update_status(memory_id=memory_id, status="deleted")
+        if entity is not None:
+            self._bump_search_cache_generation()
+        return entity
+
+    def touch_memories(self, *, memory_ids: list[str], accessed_time: datetime | None = None) -> None:
+        self.memory_repository.touch_many(
+            memory_ids=memory_ids,
+            accessed_time=accessed_time or datetime.utcnow())
+
+    def _touch_memory_access(self, memory_ids: list[str]) -> None:
+        if not memory_ids:
+            return
+        if (
+            self.settings.async_touch_enabled
+            and self.task_queue_publisher is not None
+            and self.task_queue_publisher.publish_memory_touch(memory_ids=memory_ids)
+        ):
+            return
+        self.touch_memories(memory_ids=memory_ids)
+
+    def _read_search_cache(
+        self,
+        *,
+        payload: MemorySearchRequestDto) -> list[tuple[MemoryItem, float, dict[str, float | str]]] | None:
+        if self.redis_client is None or self.settings.search_cache_ttl_seconds <= 0:
+            return None
+        raw_value = self.redis_client.get(self._search_cache_key(payload=payload))
+        if not isinstance(raw_value, (bytes, str)):
+            return None
+        decoded = raw_value.decode("utf-8") if isinstance(raw_value, bytes) else raw_value
+        try:
+            cached_items = json.loads(decoded)
+        except json.JSONDecodeError:
+            return None
+        if not isinstance(cached_items, list):
+            return None
+        results: list[tuple[MemoryItem, float, dict[str, float | str]]] = []
+        for cached_item in cached_items:
+            if not isinstance(cached_item, dict):
+                return None
+            memory_id = cached_item.get("memoryId")
+            score = cached_item.get("score")
+            score_details = cached_item.get("scoreDetails")
+            if not isinstance(memory_id, str) or not isinstance(score, (int, float)) or not isinstance(score_details, dict):
+                return None
+            item = self.memory_repository.get_by_id(memory_id=memory_id)
+            if item is None or item.status != "active":
+                return None
+            results.append((
+                item,
+                float(score),
+                {
+                    str(key): value
+                    for key, value in score_details.items()
+                    if isinstance(value, (int, float, str))
+                }))
+        return results
+
+    def _write_search_cache(
+        self,
+        *,
+        payload: MemorySearchRequestDto,
+        results: list[tuple[MemoryItem, float, dict[str, float | str]]]) -> None:
+        if self.redis_client is None or self.settings.search_cache_ttl_seconds <= 0:
+            return
+        cache_payload = [
+            {
+                "memoryId": item.id,
+                "score": score,
+                "scoreDetails": score_details,
+            }
+            for item, score, score_details in results
+        ]
+        self.redis_client.set(
+            self._search_cache_key(payload=payload),
+            json.dumps(cache_payload, ensure_ascii=False),
+            ex=self.settings.search_cache_ttl_seconds)
+
+    def _search_cache_key(self, *, payload: MemorySearchRequestDto) -> str:
+        cache_payload = {
+            **payload.model_dump(mode="json"),
+            "generation": self._read_search_cache_generation(),
+            "embeddingDimensions": self.settings.embedding_dimensions,
+            "embeddingModel": self.settings.embedding_model,
+        }
+        digest = hashlib.sha256(
+            json.dumps(cache_payload, sort_keys=True, ensure_ascii=False).encode("utf-8")
+        ).hexdigest()
+        return f"memory-search:{digest}"
+
+    def _read_search_cache_generation(self) -> int:
+        if self.redis_client is None:
+            return 0
+        value = self.redis_client.get("memory-search:generation")
+        if isinstance(value, bytes):
+            value = value.decode("utf-8")
+        if isinstance(value, str) and value.isdigit():
+            return int(value)
+        return 0
+
+    def _bump_search_cache_generation(self) -> None:
+        if self.redis_client is None:
+            return
+        try:
+            self.redis_client.incr("memory-search:generation")
+        except Exception:
+            return
 
     def _score(
         self,
@@ -107,3 +334,17 @@ class MemoryApplicationService:
             "embedding_model": item.embedding_model or self.settings.embedding_model,
             "rerank_mode": "hybrid-local",
         }
+
+
+def build_memory_application_service(
+    *,
+    db: Session,
+    settings: MemoryServiceSettings) -> MemoryApplicationService:
+    redis_client = try_build_redis_client(settings.redis_url)
+    return MemoryApplicationService(
+        memory_repository=MemoryItemRepository(db),
+        settings=settings,
+        redis_client=redis_client,
+        task_queue_publisher=(
+            TaskQueuePublisher(client=redis_client) if redis_client is not None else None
+        ))

+ 23 - 1
services/memory-service/app/bootstrap/app.py

@@ -1,3 +1,5 @@
+from contextlib import asynccontextmanager
+
 from core_shared.observability import add_observability
 from core_shared.security import add_internal_service_auth
 from fastapi import FastAPI
@@ -5,13 +7,33 @@ from fastapi import FastAPI
 from app.api.routes import router
 from app.bootstrap.settings import MemoryServiceSettings
 from app.db.session import build_session_factory
+from app.worker import BackgroundMemoryWorker, build_worker_key
+
+
+@asynccontextmanager
+async def lifespan(app: FastAPI):
+    settings: MemoryServiceSettings = app.state.settings
+    worker: BackgroundMemoryWorker | None = None
+    if settings.auto_worker_enabled:
+        worker = BackgroundMemoryWorker(
+            settings=settings,
+            session_factory=app.state.session_factory,
+            worker_key=f"api-{build_worker_key()}")
+        app.state.background_memory_worker = worker
+        worker.start()
+    try:
+        yield
+    finally:
+        if worker is not None:
+            worker.stop(timeout_seconds=settings.auto_worker_stop_timeout_seconds)
 
 
 def create_app() -> FastAPI:
     settings = MemoryServiceSettings()
     app = FastAPI(
         title="agent-platform memory-service",
-        version="0.1.0")
+        version="0.1.0",
+        lifespan=lifespan)
     app.state.settings = settings
     app.state.session_factory = build_session_factory(settings)
     add_observability(app, settings.service_name)

+ 7 - 1
services/memory-service/app/bootstrap/settings.py

@@ -4,7 +4,13 @@ from core_shared import ServiceSettings
 class MemoryServiceSettings(ServiceSettings):
     service_name: str = "memory-service"
     service_port: int = 8008
-    database_url: str = "sqlite:///./memory_service.db"
     default_search_limit: int = 8
     embedding_dimensions: int = 32
     embedding_model: str = "local-hash-v1"
+    search_cache_ttl_seconds: int = 30
+    async_touch_enabled: bool = True
+    worker_poll_interval_seconds: float = 1.0
+    worker_lease_seconds: int = 120
+    worker_max_idle_cycles: int | None = None
+    auto_worker_enabled: bool = True
+    auto_worker_stop_timeout_seconds: float = 5.0

Some files were not shown because too many files changed in this diff