8 Commits 4eb5c0463b ... 2ce2642351

Tác giả SHA1 Thông báo Ngày
  Jax Docker 2ce2642351 feat: improve team chat streaming workflow 3 tháng trước cách đây
  Jax Docker 2d35ed485a Refine models page layout 3 tháng trước cách đây
  Jax Docker 1c0d2a786e Update platform model workflow 3 tháng trước cách đây
  Jax Docker 67fe1657c6 feat: complete platform services and remove version concepts 3 tháng trước cách đây
  Jax Docker 7cdf0bb2e7 feat: connect platform services and web flows 3 tháng trước cách đây
  Jax Docker b23f861d51 Enhance site settings and header controls 3 tháng trước cách đây
  Jax Docker 7a50684696 Update platform frontend and service changes 3 tháng trước cách đây
  Jax Docker 7cb7569351 Improve teams cockpit interactions 3 tháng trước cách đây
100 tập tin đã thay đổi với 3855 bổ sung2202 xóa
  1. 17 0
      .claude/settings.local.json
  2. 0 3
      .gitignore
  3. 0 3
      .gitlab-ci.yml
  4. 19 517
      README.md
  5. 24 15
      deployments/docker/.env.example
  6. 142 171
      deployments/docker/docker-compose.yml
  7. 2 28
      deployments/docker/postgres-init/002_service_databases.sql
  8. 0 3
      deployments/docker/postgres-init/003_service_extensions.sql
  9. 0 2
      deployments/docker/prometheus.yml
  10. 56 39
      docs/auth-service-design.md
  11. 866 0
      docs/web-post-api-contract.md
  12. 1 2
      libs/core-db/src/core_db/__init__.py
  13. 1 4
      libs/core-db/src/core_db/mixins.py
  14. 6 7
      libs/core-db/src/core_db/session.py
  15. 6 36
      libs/core-domain/src/core_domain/__init__.py
  16. 2 6
      libs/core-domain/src/core_domain/agent_contracts.py
  17. 1 1
      libs/core-domain/src/core_domain/agent_tool_invocation_contracts.py
  18. 3 1
      libs/core-domain/src/core_domain/execution_contracts.py
  19. 1 1
      libs/core-domain/src/core_domain/knowledge_contracts.py
  20. 0 81
      libs/core-domain/src/core_domain/runtime_contracts.py
  21. 0 12
      libs/core-domain/src/core_domain/skill_contracts.py
  22. 3 7
      libs/core-domain/src/core_domain/team_contracts.py
  23. 3 4
      libs/core-domain/src/core_domain/tool_contracts.py
  24. 0 17
      libs/core-domain/src/core_domain/workflow_contracts.py
  25. 0 19
      libs/core-dsl/pyproject.toml
  26. 0 19
      libs/core-dsl/src/core_dsl/__init__.py
  27. 0 53
      libs/core-dsl/src/core_dsl/workflow.py
  28. 7 2
      libs/core-shared/src/core_shared/config.py
  29. 11 2
      libs/core-shared/src/core_shared/redis_primitives.py
  30. 41 0
      libs/core-shared/src/core_shared/task_queue.py
  31. 0 3
      pyproject.toml
  32. 62 5
      scripts/migrate_all.py
  33. 0 370
      scripts/smoke_runtime_no_key.py
  34. 1 1
      services/agent-service/alembic.ini
  35. 15 2
      services/agent-service/alembic/env.py
  36. 22 0
      services/agent-service/alembic/versions/20260429_9001_remove_agent_versioning.py
  37. 183 14
      services/agent-service/app/api/routes.py
  38. 277 95
      services/agent-service/app/application/services.py
  39. 0 1
      services/agent-service/app/bootstrap/settings.py
  40. 2 2
      services/agent-service/app/db/models/__init__.py
  41. 5 10
      services/agent-service/app/db/models/agent_config.py
  42. 3 3
      services/agent-service/app/db/models/agent_definition.py
  43. 4 4
      services/agent-service/app/db/models/agent_run.py
  44. 4 4
      services/agent-service/app/db/models/agent_tool_invocation.py
  45. 75 34
      services/agent-service/app/domain/repositories.py
  46. 74 7
      services/agent-service/app/infrastructure/memory_client.py
  47. 51 0
      services/agent-service/app/infrastructure/model_gateway_client.py
  48. 0 3
      services/agent-service/app/infrastructure/skill_client.py
  49. 2 2
      services/agent-service/app/infrastructure/tool_client.py
  50. 82 10
      services/agent-service/app/schemas/agent.py
  51. 1 2
      services/api-gateway/alembic.ini
  52. 15 2
      services/api-gateway/alembic/env.py
  53. 22 0
      services/api-gateway/alembic/versions/20260429_9001_remove_version_columns.py
  54. 114 105
      services/api-gateway/app/api/routes.py
  55. 0 3
      services/api-gateway/app/bootstrap/settings.py
  56. 2 2
      services/api-gateway/app/db/models/api_key.py
  57. 2 2
      services/api-gateway/app/db/models/gateway_request_audit.py
  58. 34 3
      services/api-gateway/app/infrastructure/proxy.py
  59. 22 13
      services/api-gateway/app/infrastructure/request_context.py
  60. 8 0
      services/api-gateway/app/schemas/gateway.py
  61. 1 1
      services/auth-service/alembic.ini
  62. 14 3
      services/auth-service/alembic/env.py
  63. 78 56
      services/auth-service/alembic/versions/20260425_0001_init_auth_models.py
  64. 9 0
      services/auth-service/alembic/versions/20260427_0002_add_user_password_hash.py
  65. 0 3
      services/auth-service/alembic/versions/20260427_0003_remove_auth_partition_columns.py
  66. 106 0
      services/auth-service/alembic/versions/20260428_0004_add_identity_contract_tables.py
  67. 22 0
      services/auth-service/alembic/versions/20260429_9001_remove_version_columns.py
  68. 303 0
      services/auth-service/app/api/identity_routes.py
  69. 0 149
      services/auth-service/app/api/routes.py
  70. 315 85
      services/auth-service/app/application/services.py
  71. 4 2
      services/auth-service/app/bootstrap/app.py
  72. 62 0
      services/auth-service/app/bootstrap/demo_seed.py
  73. 6 3
      services/auth-service/app/bootstrap/settings.py
  74. 3 1
      services/auth-service/app/db/models/__init__.py
  75. 17 0
      services/auth-service/app/db/models/api_key.py
  76. 2 2
      services/auth-service/app/db/models/role.py
  77. 2 2
      services/auth-service/app/db/models/role_assignment.py
  78. 15 0
      services/auth-service/app/db/models/role_permission_binding.py
  79. 2 2
      services/auth-service/app/db/models/user.py
  80. 133 1
      services/auth-service/app/domain/repositories.py
  81. 18 0
      services/auth-service/app/infrastructure/api_keys.py
  82. 0 118
      services/auth-service/app/schemas/auth.py
  83. 222 0
      services/auth-service/app/schemas/identity.py
  84. 1 1
      services/event-service/alembic.ini
  85. 15 2
      services/event-service/alembic/env.py
  86. 22 0
      services/event-service/alembic/versions/20260429_9001_remove_version_columns.py
  87. 40 0
      services/event-service/app/api/routes.py
  88. 0 1
      services/event-service/app/bootstrap/settings.py
  89. 3 3
      services/event-service/app/db/models/event_record.py
  90. 14 0
      services/event-service/app/schemas/event.py
  91. 1 1
      services/human-service/alembic.ini
  92. 15 2
      services/human-service/alembic/env.py
  93. 22 0
      services/human-service/alembic/versions/20260429_9001_remove_version_columns.py
  94. 54 0
      services/human-service/app/api/routes.py
  95. 0 1
      services/human-service/app/bootstrap/settings.py
  96. 3 3
      services/human-service/app/db/models/human_task.py
  97. 19 0
      services/human-service/app/schemas/human.py
  98. 1 1
      services/knowledge-service/alembic.ini
  99. 15 2
      services/knowledge-service/alembic/env.py
  100. 2 0
      services/knowledge-service/alembic/versions/20260425_0001_init_knowledge_models.py

+ 17 - 0
.claude/settings.local.json

@@ -0,0 +1,17 @@
+{
+  "permissions": {
+    "allow": [
+      "Read(//c/Users/Administrator/.claude/plugins/marketplaces/ui-ux-pro-max-skill/.claude/skills/ui-ux-pro-max/**)",
+      "Bash(python3 scripts/search.py \"SaaS platform dashboard admin AI agent workflow orchestration dark mode professional\" --design-system -p \"Auto Platform\")",
+      "Bash(python scripts/search.py \"SaaS platform dashboard admin AI agent workflow orchestration dark mode professional\" --design-system -p \"Auto Platform\")",
+      "Bash(python scripts/search.py \"dashboard admin panel workflow\" --domain product)",
+      "Bash(python scripts/search.py \"dark mode professional dashboard\" --domain style)",
+      "Bash(python scripts/search.py \"saas platform technology\" --domain color)",
+      "Bash(python scripts/search.py \"dashboard data analytics\" --domain typography)",
+      "Bash(python scripts/search.py \"workflow flowchart status pipeline\" --domain chart)",
+      "Bash(python scripts/search.py \"sidebar navigation dashboard admin\" --domain ux)",
+      "Bash(npm install:*)",
+      "Bash(npx tsc:*)"
+    ]
+  }
+}

+ 0 - 3
.gitignore

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

+ 0 - 3
.gitlab-ci.yml

@@ -10,12 +10,9 @@ python-test:
     - pip install -e libs/core-shared
     - pip install -e libs/core-domain
     - pip install -e libs/core-db
-    - pip install -e libs/core-dsl
     - pip install -e libs/core-events
     - pip install -e services/agent-service
     - pip install -e services/knowledge-service
-    - pip install -e services/workflow-service
-    - pip install -e services/runtime-service
   script:
     - python -m compileall libs services scripts tests
     - pytest -q

+ 19 - 517
README.md

@@ -14,8 +14,6 @@
 - `api-gateway`
 - `model-gateway-service`
 - `session-service`
-- `workflow-service`
-- `runtime-service`
 - `agent-service`
 - `memory-service`
 - `team-service`
@@ -52,8 +50,6 @@ pip install -e .\libs\core-events
 pip install -e .\libs\core-db
 pip install -e .\services\api-gateway
 pip install -e .\services\session-service
-pip install -e .\services\workflow-service
-pip install -e .\services\runtime-service
 pip install -e .\services\agent-service
 pip install -e .\services\memory-service
 pip install -e .\services\team-service
@@ -73,10 +69,10 @@ 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"
+$env:AGENT_PLATFORM_DATABASE_URL="postgresql+psycopg://user:password@localhost:5432/agent_db"
 ```
 
 ## 数据层脚手架
@@ -84,36 +80,24 @@ $env:AGENT_PLATFORM_DATABASE_URL="postgresql+psycopg://user:password@localhost:5
 本轮已经加入:
 
 - `libs/core-db`:统一 `SQLAlchemy` Base、通用 mixin、命名约定
-- `workflow-service`:应用与流程定义模型
 - `session-service`:会话与消息模型
-- `runtime-service`:运行与节点执行模型
 - `tool-service`:工具定义与绑定模型
 - 每个服务独立的 `alembic.ini`、`env.py`、`versions/`
-- `workflow-service`:已接入 repository / application service / CRUD API
 - `session-service`:已接入 repository / application service / CRUD API
 
 迁移执行示例:
 
 ```powershell
-cd D:\workspace\auto-platform\services\workflow-service
+cd D:\workspace\auto-platform\services\session-service
 alembic upgrade head
 ```
 
 其他服务同理:
 
-- `services/session-service`
-- `services/runtime-service`
 - `services/tool-service`
 
 接口示例:
 
-```powershell
-Invoke-RestMethod -Method Post `
-  -Uri http://127.0.0.1:8002/workflows/apps `
-  -ContentType "application/json" `
-  -Body '{"code":"sales_assistant","name":"Sales Assistant"}'
-```
-
 ```powershell
 Invoke-RestMethod -Method Post `
   -Uri http://127.0.0.1:8001/sessions `
@@ -121,45 +105,6 @@ Invoke-RestMethod -Method Post `
   -Body '{"app_id":"app-1","user_id":"user-1","channel_type":"web"}'
 ```
 
-```powershell
-Invoke-RestMethod -Method Post `
-  -Uri http://127.0.0.1:8002/workflows/versions `
-  -ContentType "application/json" `
-  -Body '{"workflow_id":"wf-1","dsl_json":{"nodes":[],"edges":[]}}'
-```
-
-```powershell
-Invoke-RestMethod -Method Post `
-  -Uri http://127.0.0.1:8001/sessions/run-requests `
-  -ContentType "application/json" `
-  -Body '{"session_id":"sess-1","app_version_id":"appv-1","workflow_version_id":"wfv-1"}'
-```
-
-```powershell
-Invoke-RestMethod -Method Post `
-  -Uri http://127.0.0.1:8003/runtime/runs `
-  -ContentType "application/json" `
-  -Body '{"app_id":"app-1","app_version_id":"appv-1","workflow_id":"wf-1","workflow_version_id":"wfv-1","session_id":"sess-1","initial_node":{"node_id":"start","node_type":"llm"}}'
-```
-
-如果不传 `initial_node`,`runtime-service` 会调用 `workflow-service` 读取对应的 `workflow version`,并从 DSL 中自动推导首节点:
-
-```powershell
-Invoke-RestMethod -Method Post `
-  -Uri http://127.0.0.1:8003/runtime/runs `
-  -ContentType "application/json" `
-  -Body '{"app_id":"app-1","app_version_id":"appv-1","workflow_id":"wf-1","workflow_version_id":"wfv-1","session_id":"sess-1"}'
-```
-
-一条链直接派发到 runtime:
-
-```powershell
-Invoke-RestMethod -Method Post `
-  -Uri http://127.0.0.1:8001/sessions/run-requests/dispatch `
-  -ContentType "application/json" `
-  -Body '{"session_id":"sess-1","app_id":"app-1","app_version_id":"appv-1","workflow_id":"wf-1","workflow_version_id":"wfv-1","initial_node":{"node_id":"start","node_type":"llm"}}'
-```
-
 工具定义示例:
 
 ```powershell
@@ -176,36 +121,12 @@ Invoke-RestMethod -Method Post `
   -Body '{"tool_id":"tool-1","input_schema_json":{"query":{"type":"string"}},"invoke_config_json":{"method":"GET","path":"/products/search"}}'
 ```
 
-运行状态推进示例:
-
-```powershell
-Invoke-RestMethod -Method Post `
-  -Uri http://127.0.0.1:8003/runtime/node-runs/node-run-id/status `
-  -ContentType "application/json" `
-  -Body '{"status":"running","worker_key":"runtime-worker-1"}'
-```
-
-```powershell
-Invoke-RestMethod -Method Post `
-  -Uri http://127.0.0.1:8003/runtime/runs/run-id/status `
-  -ContentType "application/json" `
-  -Body '{"status":"completed"}'
-```
-
-说明:
-
-- 当你调用 `node-runs/{node_run_id}/status` 更新节点状态时,`runtime-service` 会自动聚合当前运行下所有 `node_run` 的状态,并同步刷新 `workflow_run.status`
-- 当前规则是:任一节点 `failed` 则运行 `failed`;有节点 `running` 则运行 `running`;全部节点都为 `completed/skipped` 则运行 `completed`
-- 当某个 `node_run` 被更新为 `completed` 时,`runtime-service` 还会基于 `workflow version` 的 DSL 自动查找后继节点,并创建新的 `queued` 状态 `node_run`
-
 ## 目录结构
 
 ```text
 services/
   api-gateway/
   session-service/
-  workflow-service/
-  runtime-service/
   skill-service/
   human-service/
   knowledge-service/
@@ -232,9 +153,7 @@ tests/
 2. 写第一版 Alembic 初始迁移
 3. 接入 PostgreSQL / Redis
 4. 增加 Docker Compose
-5. 开始实现应用、流程、运行三条主链路
-
-## Runtime Execute APIs
+5. 开始实现会话、代理、团队主链路
 
 ## Agent Service APIs
 
@@ -384,7 +303,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
@@ -469,23 +388,6 @@ Invoke-RestMethod -Method Post `
 
 Through `api-gateway`, use `/gateway/human/**`.
 
-Runtime human-in-the-loop nodes now create `human-service` tasks and pause the
-node in `pending` status until the task is completed. Supported node types:
-
-- `human`
-- `approval`
-- `human-input`
-- `human-takeover`
-
-After completing the human task, resume the blocked node:
-
-```powershell
-Invoke-RestMethod -Method Post `
-  -Uri http://127.0.0.1:8003/runtime/node-runs/node-run-id/resume-human `
-  -ContentType "application/json" `
-  -Body '{"human_task_id":"human-task-id","worker_key":"runtime-worker-1"}'
-```
-
 ## Knowledge Service APIs
 
 `knowledge-service` stores independent knowledge bases, documents, chunks, and
@@ -497,8 +399,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:
 
@@ -541,7 +443,7 @@ Publish an event:
 Invoke-RestMethod -Method Post `
   -Uri http://127.0.0.1:8013/events `
   -ContentType "application/json" `
-  -Body '{"event_type":"run.created","source_service":"runtime-service","aggregate_type":"workflow_run","aggregate_id":"run-id","payload_json":{"run_id":"run-id"}}'
+  -Body '{"event_type":"run.created","source_service":"agent-service","aggregate_type":"agent_run","aggregate_id":"run-id","payload_json":{"run_id":"run-id"}}'
 ```
 
 Claim pending events for a delivery worker:
@@ -579,7 +481,7 @@ Invoke-RestMethod -Method Post `
 Invoke-RestMethod -Method Post `
   -Uri http://127.0.0.1:8014/auth/permissions/check `
   -ContentType "application/json" `
-  -Body "{`"user_id`":`"$($user.id)`",`"permission`":`"workflow:write`"}"
+  -Body "{`"user_id`":`"$($user.id)`",`"permission`":`"agent:write`"}"
 ```
 
 Through `api-gateway`, use `/gateway/auth/**`.
@@ -588,7 +490,7 @@ Through `api-gateway`, use `/gateway/auth/**`.
 
 `scheduler-service` stores delayed jobs and due-job leases for time-based
 automation. It is intentionally service-neutral: jobs can target HTTP,
-event, runtime, agent, or team execution.
+event, agent, or team execution.
 
 Create a scheduled job:
 
@@ -596,7 +498,7 @@ Create a scheduled job:
 Invoke-RestMethod -Method Post `
   -Uri http://127.0.0.1:8015/scheduler/jobs `
   -ContentType "application/json" `
-  -Body '{"job_type":"runtime","name":"Run workflow later","schedule_time":"2026-04-26T12:00:00Z","payload_json":{"workflow_run_id":"run-id"}}'
+  -Body '{"job_type":"agent","name":"Run agent later","schedule_time":"2026-04-26T12:00:00Z","payload_json":{"agent_run_id":"run-id"}}'
 ```
 
 Claim due jobs for a worker:
@@ -623,7 +525,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,318 +587,18 @@ 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
 ```
 
-`runtime-service` now includes a typed executor skeleton for these node types:
-
-- `llm`
-- `tool`
-- `code`
-- `human`
-- `approval`
-- `human-input`
-- `human-takeover`
-- `answer`
-- `if-else`
-- `assigner`
-- `knowledge-retrieval`
-- `template-transform`
-
-Execute a specific queued node:
-
-```powershell
-Invoke-RestMethod -Method Post `
-  -Uri http://127.0.0.1:8003/runtime/node-runs/node-run-id/execute `
-  -ContentType "application/json" `
-  -Body '{"worker_key":"runtime-worker-1"}'
-```
-
-Execute the next queued node in a run:
-
-```powershell
-Invoke-RestMethod -Method Post `
-  -Uri "http://127.0.0.1:8003/runtime/runs/run-id/execute-next" `
-  -ContentType "application/json" `
-  -Body '{"worker_key":"runtime-worker-1"}'
-```
-
-Execute queued nodes in sequence until the run is finished, blocked, or reaches `max_steps`:
-
-```powershell
-Invoke-RestMethod -Method Post `
-  -Uri "http://127.0.0.1:8003/runtime/runs/run-id/execute" `
-  -ContentType "application/json" `
-  -Body '{"worker_key":"runtime-worker-1","max_steps":16}'
-```
-
-Execute one queued node through the worker claim API:
-
-```powershell
-Invoke-RestMethod -Method Post `
-  -Uri "http://127.0.0.1:8003/runtime/workers/execute-next" `
-  -ContentType "application/json" `
-  -Body '{"worker_key":"runtime-worker-1","lease_seconds":300}'
-```
-
-Run a standalone runtime worker process:
-
-```powershell
-Push-Location .\services\runtime-service
-$env:AGENT_PLATFORM_DATABASE_URL="sqlite:///./runtime_service.db"
-..\..\.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.
-
-Node execution results are now persisted on `node_run`:
-
-- `output_text`
-- `output_json`
-
-Node execution artifacts are also persisted on `node_artifact`:
-
-- `artifact_type`
-- `content_text`
-- `content_json`
-- `storage_uri`
-- `size_bytes`
-
-Query artifacts:
-
-```powershell
-Invoke-RestMethod `
-  -Uri "http://127.0.0.1:8003/runtime/node-artifacts?run_id=run-id"
-```
-
-Trace spans are persisted on `trace_span` for timeline and latency analysis:
-
-- `span_type`
-- `name`
-- `status`
-- `started_time`
-- `ended_time`
-- `duration_ms`
-- `attributes_json`
-- `error_code`
-- `error_message`
-
-Query trace spans:
-
-```powershell
-Invoke-RestMethod `
-  -Uri "http://127.0.0.1:8003/runtime/trace-spans?run_id=run-id"
-```
-
-Current behavior:
-
-- `answer` nodes persist rendered text to `output_text`
-- `assigner` nodes write `state_updates` to `output_json`
-- `condition` / `if-else` nodes write `condition_result` and `route` to `output_json`
-- `template-transform` nodes render text or JSON using previous node outputs and run state
-- `knowledge-retrieval` / `retriever` nodes run keyword retrieval over inline or HTTP JSON documents
-- `tool` nodes persist resolved binding/tool metadata to `output_json`
-- default executors persist basic executor metadata to `output_json`
-- parallel fan-out is supported by defining multiple outgoing edges from one node
-- join nodes wait for predecessor completion with `config.join_policy`
-- loop/re-entry is supported with `config.allow_loop=true` and `config.max_iterations`
-- retry is supported with `config.retry_policy.max_attempts` and `retry_delay_seconds`
-- delayed scheduling and node timeout use `config.delay_seconds` and `config.timeout_seconds`
-- compensation nodes can be queued on failure with `config.compensation_node_id`
-
-Runtime template context:
-
-- `state.xxx`: values written by previous `assigner` nodes
-- `nodes.node_id.output.xxx`: structured output from a previous node
-- `nodes.node_id.text`: text output from a previous node
-- `current.node_id`: current node id
-
-Assigner node config example:
-
-```json
-{
-  "id": "seed-state",
-  "type": "assigner",
-  "config": {
-    "assignments": {
-      "score": 7,
-      "user_name": "Alice"
-    }
-  }
-}
-```
-
-Condition node config example:
-
-```json
-{
-  "id": "check-score",
-  "type": "if-else",
-  "config": {
-    "expression": "state.score >= 5"
-  }
-}
-```
-
-Conditional edge example:
-
-```json
-[
-  {"source": "check-score", "target": "high-path", "condition": "true"},
-  {"source": "check-score", "target": "low-path", "condition": "false"}
-]
-```
-
-Join node config example:
-
-```json
-{
-  "id": "join-results",
-  "type": "join",
-  "config": {
-    "join_policy": "all_completed"
-  }
-}
-```
-
-Loop and retry config example:
-
-```json
-{
-  "id": "poll-status",
-  "type": "tool",
-  "config": {
-    "allow_loop": true,
-    "max_iterations": 5,
-    "timeout_seconds": 30,
-    "retry_policy": {
-      "max_attempts": 3,
-      "retry_delay_seconds": 2
-    }
-  }
-}
-```
-
-Compensation config example:
-
-```json
-{
-  "id": "charge-card",
-  "type": "tool",
-  "config": {
-    "compensation_node_id": "refund-card"
-  }
-}
-```
-
-Template node config example:
-
-```json
-{
-  "id": "high-path",
-  "type": "template-transform",
-  "config": {
-    "template": "{{state.user_name}} passed with score {{state.score}}"
-  }
-}
-```
-
-Retriever node config example:
-
-```json
-{
-  "id": "retrieve-docs",
-  "type": "knowledge-retrieval",
-  "config": {
-    "query_template": "{{state.query}}",
-    "top_k": 2,
-    "documents": [
-      {
-        "id": "refund",
-        "title": "Refund Policy",
-        "text": "Refund policy allows returns within seven days."
-      },
-      {
-        "id": "shipping",
-        "title": "Shipping Policy",
-        "text": "Shipping usually takes three to five business days."
-      }
-    ]
-  }
-}
-```
-
-Retriever nodes can call `knowledge-service` directly:
-
-```json
-{
-  "id": "retrieve-kb",
-  "type": "knowledge-retrieval",
-  "config": {
-    "knowledge_base_id": "kb-id",
-    "query_template": "{{state.query}}",
-    "top_k": 3,
-    "filters_json": {
-      "source_type": "text"
-    }
-  }
-}
-```
-
-Retriever output is persisted to `node_run.output_json.retrieved_documents`. Template nodes can consume it:
-
-```json
-{
-  "id": "render-answer",
-  "type": "template-transform",
-  "config": {
-    "template": "Top doc: {{nodes.retrieve-docs.output.retrieved_documents.0.title}}"
-  }
-}
-```
-
-Retriever nodes can also load documents from an HTTP JSON source:
-
-```json
-{
-  "id": "retrieve-remote-docs",
-  "type": "retriever",
-  "config": {
-    "query": "refund policy",
-    "source_url": "http://127.0.0.1:9000/documents",
-    "top_k": 3
-  }
-}
-```
-
-The HTTP source should return either a document list or an object with a `documents` list.
-
-Run the no-key runtime smoke test after local services are running:
-
-```powershell
-.\.venv\Scripts\python scripts\smoke_runtime_no_key.py
-```
-
-Run the same smoke test through `api-gateway`:
-
-```powershell
-$env:AGENT_PLATFORM_SMOKE_WORKFLOW_URL="http://127.0.0.1:8000/gateway/workflows"
-$env:AGENT_PLATFORM_SMOKE_RUNTIME_URL="http://127.0.0.1:8000/gateway/runtime"
-.\.venv\Scripts\python scripts\smoke_runtime_no_key.py
-```
-
 ## API Gateway
 
 `api-gateway` provides a unified entrypoint:
 
 - `GET /gateway/services/health`
-- `/gateway/workflows/**` -> `workflow-service /workflows/**`
 - `/gateway/sessions/**` -> `session-service /sessions/**`
-- `/gateway/runtime/**` -> `runtime-service /runtime/**`
 - `/gateway/agents/**` -> `agent-service /agents/**`
 - `/gateway/memories/**` -> `memory-service /memories/**`
 - `/gateway/teams/**` -> `team-service /teams/**`
@@ -1060,7 +662,7 @@ Create an API key:
 ```powershell
 $body = @{
     name = "local-dev"
-  scopes = "gateway:agents:* gateway:runtime:read"
+  scopes = "gateway:agents:*"
 } | ConvertTo-Json
 
 $created = Invoke-RestMethod `
@@ -1095,74 +697,6 @@ Invoke-RestMethod `
   -Body $body
 ```
 
-Run smoke test through an authenticated gateway:
-
-```powershell
-$env:AGENT_PLATFORM_SMOKE_WORKFLOW_URL="http://127.0.0.1:8000/gateway/workflows"
-$env:AGENT_PLATFORM_SMOKE_RUNTIME_URL="http://127.0.0.1:8000/gateway/runtime"
-$env:AGENT_PLATFORM_SMOKE_API_KEY=$created.api_key
-.\.venv\Scripts\python scripts\smoke_runtime_no_key.py
-```
-
-HTTP tool node config example:
-
-```json
-{
-  "id": "search-products",
-  "type": "tool",
-  "config": {
-    "tool_binding_id": "binding-1",
-    "query": {
-      "keyword": "milk"
-    }
-  }
-}
-```
-
-Supported HTTP tool config resolution order:
-
-- URL: `config.url` or `invoke_config_json.url`
-- Base URL: `config.base_url` or `binding.config_json.base_url` or `invoke_config_json.base_url`
-- Path: `config.path` or `invoke_config_json.path`
-- Method: `invoke_config_json.method`, default `GET`
-- Query params: merge `invoke_config_json.query` + `config.query`
-- Body JSON: merge `invoke_config_json.body` + `config.body`
-- Headers: merge `invoke_config_json.headers` + `binding.config_json.headers` + `config.headers`
-
-LLM node config example:
-
-```json
-{
-  "id": "draft-answer",
-  "type": "llm",
-  "config": {
-    "model": "gpt-4o-mini",
-    "system_prompt": "You are a customer support assistant.",
-    "prompt": "Summarize the user intent in Chinese.",
-    "temperature": 0.2,
-    "max_tokens": 400
-  }
-}
-```
-
-`llm` nodes also support explicit `messages`:
-
-```json
-{
-  "id": "rewrite-message",
-  "type": "llm",
-  "config": {
-    "model": "gpt-4o-mini",
-    "messages": [
-      {"role": "system", "content": "You are a concise editor."},
-      {"role": "user", "content": "Rewrite this sentence in a warmer tone."}
-    ]
-  }
-}
-```
-
-`runtime-service` sends `llm` execution requests to `model-gateway-service`, and the gateway forwards them to an OpenAI-compatible `/chat/completions` provider.
-
 Recommended environment variables for `model-gateway-service`:
 
 ```powershell
@@ -1171,30 +705,6 @@ $env:AGENT_PLATFORM_PROVIDER_API_KEY="your-api-key"
 $env:AGENT_PLATFORM_DEFAULT_MODEL="gpt-4o-mini"
 ```
 
-Code node config example:
-
-```json
-{
-  "id": "compute-summary",
-  "type": "code",
-  "config": {
-    "language": "python",
-    "timeout_seconds": 5,
-    "input_json": {
-      "numbers": [1, 2, 3, 4]
-    },
-    "code": "total = sum(payload['numbers'])\nresult = {'total': total, 'count': len(payload['numbers'])}\nprint(f'total={total}')"
-  }
-}
-```
-
-`runtime-service` sends `code` execution requests to `code-runner-service`. Current `python` execution contract:
-
-- input payload is available as `payload`
-- execution result should be assigned to `result`
-- `print(...)` output is captured into `node_run.output_text`
-- structured `result` is captured into `node_run.output_json.result_json`
-
 Recommended environment variables for `code-runner-service`:
 
 ```powershell
@@ -1228,7 +738,7 @@ Production-like infrastructure:
 
 - Compose now starts `postgres` with the `pgvector` image and runs `CREATE EXTENSION IF NOT EXISTS vector`.
 - Compose now starts durable `redis` with append-only persistence.
-- Copy `deployments/docker/.env.example` to `.env` to use per-service PostgreSQL databases such as `workflow_service`, `agent_service`, and `knowledge_service`.
+- Copy `deployments/docker/.env.example` to `.env` to use per-service PostgreSQL databases such as `agent_service` and `knowledge_service`.
 - Set `AGENT_PLATFORM_REDIS_URL=redis://redis:6379/0` to enable shared Redis-backed locks, idempotency keys, and queues.
 
 Run all service migrations:
@@ -1240,7 +750,7 @@ python .\scripts\migrate_all.py
 Run only selected migrations:
 
 ```powershell
-python .\scripts\migrate_all.py --only agent-service --only runtime-service
+python .\scripts\migrate_all.py --only agent-service --only knowledge-service
 ```
 
 Run the automated smoke tests:
@@ -1254,12 +764,6 @@ The repository includes `.gitlab-ci.yml` with a Python 3.11 test job that
 installs the core libraries plus Agent/Knowledge services, runs `compileall`,
 and executes the pytest smoke suite.
 
-Scale runtime workers:
-
-```powershell
-docker compose -f .\deployments\docker\docker-compose.yml up --build -d --scale runtime-worker=3
-```
-
 Scale agent workers:
 
 ```powershell
@@ -1286,10 +790,10 @@ 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-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`
 - `memory-service` stores scoped memories under `/data`; move it to PostgreSQL before enabling high-volume memory writes
 - `team-service` stores multi-agent team definitions, team versions, and team run records under `/data`
@@ -1302,6 +806,4 @@ Important notes:
 - `scheduler-service` stores delayed jobs, due-job leases, and retry status under `/data`
 - `agent-worker` has no exposed port and can be scaled independently; set `AGENT_PLATFORM_AGENT_WORKER_DRY_RUN=true` for no-key local smoke runs
 - `scheduler-worker` has no exposed port and can be scaled independently; prefer PostgreSQL for real multi-worker write concurrency
-- `runtime-worker` has no exposed port and can be scaled independently; prefer PostgreSQL for real multi-worker write concurrency
-- `runtime-service` automatically resolves internal URLs to `workflow-service`, `tool-service`, `model-gateway-service`, and `code-runner-service`
 - `model-gateway-service` defaults to `http://host.docker.internal:11434/v1`; replace it in `.env` if you want OpenAI or another OpenAI-compatible provider

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

@@ -4,22 +4,21 @@ 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_SESSION_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 +27,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 +44,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

+ 142 - 171
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
@@ -53,10 +88,6 @@ services:
         condition: service_started
       session-service:
         condition: service_started
-      workflow-service:
-        condition: service_started
-      runtime-service:
-        condition: service_started
       tool-service:
         condition: service_started
       model-gateway-service:
@@ -82,28 +113,6 @@ services:
       scheduler-service:
         condition: service_started
 
-  workflow-service:
-    build:
-      context: ../..
-      dockerfile: deployments/docker/python-service.Dockerfile
-      args:
-        SERVICE_PATH: services/workflow-service
-    container_name: agent-platform-workflow-service
-    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}
-    ports:
-      - "8002:8002"
-    volumes:
-      - workflow_service_data:/data
-    healthcheck:
-      test: ["CMD", "python", "-c", "import urllib.request; urllib.request.urlopen('http://127.0.0.1:8002/workflows/health').read()"]
-      interval: 15s
-      timeout: 5s
-      retries: 5
-
   session-service:
     build:
       context: ../..
@@ -114,16 +123,11 @@ 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_RUNTIME_SERVICE_URL: http://runtime-service:8003
+      AGENT_PLATFORM_DATABASE_URL: ${AGENT_PLATFORM_SESSION_DATABASE_URL:-postgresql+psycopg://admin:hFOvG5UBeK5KIGhz5cQH@git.newpoint.work:5432/vectordb}
     ports:
       - "8001:8001"
     volumes:
       - session_service_data:/data
-    depends_on:
-      runtime-service:
-        condition: service_started
     healthcheck:
       test: ["CMD", "python", "-c", "import urllib.request; urllib.request.urlopen('http://127.0.0.1:8001/sessions/health').read()"]
       interval: 15s
@@ -140,8 +144,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 +155,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 +185,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 +229,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 +265,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 +298,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 +309,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 +338,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 +360,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 +384,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 +405,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 +426,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 +435,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 +501,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,13 +523,12 @@ 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:
       - auth_service_data:/data
     healthcheck:
-      test: ["CMD", "python", "-c", "import urllib.request; urllib.request.urlopen('http://127.0.0.1:8014/auth/health').read()"]
+      test: ["CMD", "python", "-c", "import urllib.request; urllib.request.urlopen('http://127.0.0.1:8014/identity/health').read()"]
       interval: 15s
       timeout: 5s
       retries: 5
@@ -465,8 +543,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 +563,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}
@@ -500,103 +576,6 @@ services:
       event-service:
         condition: service_started
 
-  runtime-service:
-    build:
-      context: ../..
-      dockerfile: deployments/docker/python-service.Dockerfile
-      args:
-        SERVICE_PATH: services/runtime-service
-    container_name: agent-platform-runtime-service
-    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_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
-      AGENT_PLATFORM_CODE_RUNNER_SERVICE_URL: http://code-runner-service:8006
-      AGENT_PLATFORM_AGENT_SERVICE_URL: http://agent-service:8007
-      AGENT_PLATFORM_MEMORY_SERVICE_URL: http://memory-service:8008
-      AGENT_PLATFORM_TEAM_SERVICE_URL: http://team-service:8009
-      AGENT_PLATFORM_SKILL_SERVICE_URL: http://skill-service:8010
-      AGENT_PLATFORM_HUMAN_SERVICE_URL: http://human-service:8011
-      AGENT_PLATFORM_KNOWLEDGE_SERVICE_URL: http://knowledge-service:8012
-      AGENT_PLATFORM_EVENT_SERVICE_URL: http://event-service:8013
-      AGENT_PLATFORM_SCHEDULER_SERVICE_URL: http://scheduler-service:8015
-    ports:
-      - "8003:8003"
-    volumes:
-      - runtime_service_data:/data
-    depends_on:
-      workflow-service:
-        condition: service_started
-      tool-service:
-        condition: service_started
-      model-gateway-service:
-        condition: service_started
-      code-runner-service:
-        condition: service_started
-      agent-service:
-        condition: service_started
-      memory-service:
-        condition: service_started
-      team-service:
-        condition: service_started
-      skill-service:
-        condition: service_started
-      human-service:
-        condition: service_started
-      knowledge-service:
-        condition: service_started
-      event-service:
-        condition: service_started
-      scheduler-service:
-        condition: service_started
-    healthcheck:
-      test: ["CMD", "python", "-c", "import urllib.request; urllib.request.urlopen('http://127.0.0.1:8003/runtime/health').read()"]
-      interval: 15s
-      timeout: 5s
-      retries: 5
-
-  runtime-worker:
-    build:
-      context: ../..
-      dockerfile: deployments/docker/python-service.Dockerfile
-      args:
-        SERVICE_PATH: services/runtime-service
-    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_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
-      AGENT_PLATFORM_CODE_RUNNER_SERVICE_URL: http://code-runner-service:8006
-      AGENT_PLATFORM_KNOWLEDGE_SERVICE_URL: http://knowledge-service:8012
-      AGENT_PLATFORM_EVENT_SERVICE_URL: http://event-service:8013
-      AGENT_PLATFORM_SCHEDULER_SERVICE_URL: http://scheduler-service:8015
-      AGENT_PLATFORM_WORKER_POLL_INTERVAL_SECONDS: ${AGENT_PLATFORM_WORKER_POLL_INTERVAL_SECONDS:-1}
-      AGENT_PLATFORM_WORKER_LEASE_SECONDS: ${AGENT_PLATFORM_WORKER_LEASE_SECONDS:-300}
-    volumes:
-      - runtime_service_data:/data
-    depends_on:
-      workflow-service:
-        condition: service_started
-      tool-service:
-        condition: service_started
-      model-gateway-service:
-        condition: service_started
-      code-runner-service:
-        condition: service_started
-      knowledge-service:
-        condition: service_started
-      event-service:
-        condition: service_started
-      scheduler-service:
-        condition: service_started
-
   api-gateway:
     build:
       context: ../..
@@ -607,11 +586,8 @@ 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_WORKFLOW_SERVICE_URL: http://workflow-service:8002
+      AGENT_PLATFORM_DATABASE_URL: ${AGENT_PLATFORM_API_GATEWAY_DATABASE_URL:-postgresql+psycopg://admin:hFOvG5UBeK5KIGhz5cQH@git.newpoint.work:5432/vectordb}
       AGENT_PLATFORM_SESSION_SERVICE_URL: http://session-service:8001
-      AGENT_PLATFORM_RUNTIME_SERVICE_URL: http://runtime-service:8003
       AGENT_PLATFORM_TOOL_SERVICE_URL: http://tool-service:8004
       AGENT_PLATFORM_MODEL_GATEWAY_SERVICE_URL: http://model-gateway-service:8005
       AGENT_PLATFORM_CODE_RUNNER_SERVICE_URL: http://code-runner-service:8006
@@ -634,12 +610,8 @@ services:
     volumes:
       - api_gateway_data:/data
     depends_on:
-      workflow-service:
-        condition: service_started
       session-service:
         condition: service_started
-      runtime-service:
-        condition: service_started
       tool-service:
         condition: service_started
       model-gateway-service:
@@ -673,6 +645,7 @@ services:
 volumes:
   postgres_data:
   redis_data:
+  minio_data:
   prometheus_data:
   api_gateway_data:
   agent_service_data:
@@ -684,8 +657,6 @@ volumes:
   event_service_data:
   auth_service_data:
   scheduler_service_data:
-  workflow_service_data:
   session_service_data:
-  runtime_service_data:
   tool_service_data:
   model_gateway_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;

+ 0 - 2
deployments/docker/prometheus.yml

@@ -9,8 +9,6 @@ scrape_configs:
       - targets:
           - api-gateway:8000
           - session-service:8001
-          - workflow-service:8002
-          - runtime-service:8003
           - tool-service:8004
           - model-gateway-service:8005
           - code-runner-service:8006

+ 56 - 39
docs/auth-service-design.md

@@ -1,8 +1,8 @@
-# auth-service design
+# identity-service design
 
 ## Scope
 
-`auth-service` owns Web Studio account/password login, users, roles, role assignments, and permission checks.
+`identity-service` owns Web Studio account/password login, users, roles, role assignments, and permission checks.
 
 The auth domain is single-workspace. Auth API payloads and auth tables do not carry workspace partition fields.
 
@@ -10,17 +10,17 @@ The auth domain is single-workspace. Auth API payloads and auth tables do not ca
 
 Frontend requests go through `/gateway`:
 
-- `POST /gateway/auth/login`
-- `GET /gateway/auth/users`
-- `GET /gateway/auth/roles`
-- `POST /gateway/auth/permissions/check`
+- `POST /gateway/identity/auth/login`
+- `POST /gateway/identity/users/list`
+- `POST /gateway/identity/roles/list`
+- `POST /gateway/identity/permissions/check`
 
-Gateway proxies them to auth-service:
+Gateway proxies them to identity-service:
 
-- `POST /auth/login`
-- `GET /auth/users`
-- `GET /auth/roles`
-- `POST /auth/permissions/check`
+- `POST /identity/auth/login`
+- `POST /identity/users/list`
+- `POST /identity/roles/list`
+- `POST /identity/permissions/check`
 
 After login, frontend sends:
 
@@ -59,11 +59,19 @@ $env:AGENT_PLATFORM_DATABASE_URL="postgresql+psycopg://admin:hFOvG5UBeK5KIGhz5cQ
 ### auth_role
 
 - `id`
-- `code`
 - `name`
 - `description`
 - `status`: `active | disabled`
-- `permissions_json`
+- audit fields
+- `version`
+
+### auth_role_permission_binding
+
+- `id`
+- `role_id`
+- `permission`
+- `scope_type`
+- `scope_id`
 - audit fields
 - `version`
 
@@ -81,7 +89,7 @@ $env:AGENT_PLATFORM_DATABASE_URL="postgresql+psycopg://admin:hFOvG5UBeK5KIGhz5cQ
 
 ## Login
 
-`POST /auth/login`
+`POST /identity/auth/login`
 
 ```json
 {
@@ -94,18 +102,21 @@ Response:
 
 ```json
 {
-  "access_token": "apt_xxx",
-  "token_type": "bearer",
-  "expires_time": "2026-04-28T07:10:00Z",
-  "user": {
-    "id": "user-id",
-    "username": "demo-user",
-    "display_name": "Demo User",
-    "email": "demo@example.com",
-    "status": "active",
-    "metadata_json": {},
-    "last_login_time": "2026-04-27T23:10:00Z",
-    "created_time": "2026-04-27T23:00:00Z"
+  "success": true,
+  "data": {
+    "accessToken": "apt_xxx",
+    "tokenType": "bearer",
+    "expiresTime": "2026-04-28T07:10:00Z",
+    "user": {
+      "id": "user-id",
+      "username": "demo-user",
+      "displayName": "Demo User",
+      "email": "demo@example.com",
+      "metadata": {},
+      "lastLoginTime": "2026-04-27T23:10:00Z",
+      "createdTime": "2026-04-27T23:00:00Z",
+      "updatedTime": "2026-04-27T23:00:00Z"
+    }
   }
 }
 ```
@@ -114,11 +125,11 @@ Passwords are stored with salted `PBKDF2-HMAC-SHA256`. Access tokens are HMAC si
 
 ## Token Verification
 
-`POST /auth/tokens/verify`
+`POST /identity/auth/tokens/verify`
 
 ```json
 {
-  "access_token": "apt_xxx"
+  "accessToken": "apt_xxx"
 }
 ```
 
@@ -126,23 +137,26 @@ Response:
 
 ```json
 {
-  "active": true,
-  "user_id": "user-id",
-  "username": "demo-user",
-  "expires_time": "2026-04-28T07:10:00"
+  "success": true,
+  "data": {
+    "active": true,
+    "userId": "user-id",
+    "username": "demo-user",
+    "expiresTime": "2026-04-28T07:10:00"
+  }
 }
 ```
 
 ## Permission Check
 
-`POST /auth/permissions/check`
+`POST /identity/permissions/check`
 
 ```json
 {
-  "user_id": "user-id",
+  "userId": "user-id",
   "permission": "workflow:read",
-  "scope_type": null,
-  "scope_id": null
+  "scopeType": null,
+  "scopeId": null
 }
 ```
 
@@ -150,9 +164,12 @@ Response:
 
 ```json
 {
-  "allowed": true,
-  "reason": "matched",
-  "matched_role_ids": ["role-id"]
+  "success": true,
+  "data": {
+    "allowed": true,
+    "reason": "matched",
+    "matchedRoleIds": ["role-id"]
+  }
 }
 ```
 

+ 866 - 0
docs/web-post-api-contract.md

@@ -0,0 +1,866 @@
+# Web 前端业务闭环与多服务 POST API 合约
+
+本文档定义前端与后端服务之间的目标 API 合约。所有接口统一使用 `POST`,所有传输字段统一使用小驼峰,所有时间字段统一以 `Time` 结尾并使用 `datetime` 类型。多对多关系必须由中间表资源维护,前端可以一次性提交勾选结果,但服务端必须拆分落到绑定表。
+
+## 1. 总体约定
+
+### 1.1 请求与响应
+
+| 项 | 约定 |
+| --- | --- |
+| Base URL | `/gateway` |
+| Method | 全部使用 `POST` |
+| Content-Type | `application/json; charset=utf-8` |
+| Auth Header | `Authorization: Bearer <accessToken>` |
+| User Header | `x-user-id: <userId>` |
+| Request Header | `x-request-id: <requestId>`,可选 |
+| 字段命名 | 前端和后端 API 传输层全部使用小驼峰 |
+| 时间字段 | 字段名必须以 `Time` 结尾,类型必须是 `datetime` |
+| 关系字段 | 单对象关联使用 `xxxId`,多对多关系必须使用绑定资源 |
+| 禁止传输层字段 | 不使用 `enabled`、版本字段、智能体类型、团队类型、给用户看的业务编号字段 |
+| 数据库命名 | 数据库存储命名由服务内部处理,不能泄漏到 API DTO |
+
+统一响应包:
+
+| 字段 | 类型 | 必填 | 说明 |
+| --- | --- | --- | --- |
+| `success` | boolean | 是 | 是否成功 |
+| `data` | object \| array \| null | 是 | 成功数据 |
+| `error` | `ApiError` \| null | 是 | 错误信息 |
+| `requestId` | string | 是 | 请求追踪 ID |
+| `serverTime` | datetime | 是 | 服务端响应时间 |
+
+`ApiError`
+
+| 字段 | 类型 | 必填 | 说明 |
+| --- | --- | --- | --- |
+| `errorType` | string | 是 | 机器可读错误类型 |
+| `message` | string | 是 | 用户可读错误消息 |
+| `details` | object | 否 | 字段错误或诊断信息 |
+
+`PageRequest`
+
+| 字段 | 类型 | 必填 | 默认 | 说明 |
+| --- | --- | --- | --- | --- |
+| `page` | integer | 否 | `1` | 页码 |
+| `pageSize` | integer | 否 | `20` | 每页数量,最大 200 |
+| `keyword` | string | 否 | 空 | 搜索关键字 |
+| `sortBy` | string | 否 | `createdTime` | 排序字段 |
+| `sortOrder` | `"asc"` \| `"desc"` | 否 | `"desc"` | 排序方向 |
+
+`PageResult<T>`
+
+| 字段 | 类型 | 必填 | 说明 |
+| --- | --- | --- | --- |
+| `items` | `T[]` | 是 | 当前页数据 |
+| `total` | integer | 是 | 总数量 |
+| `page` | integer | 是 | 当前页码 |
+| `pageSize` | integer | 是 | 每页数量 |
+| `hasMore` | boolean | 是 | 是否还有下一页 |
+
+### 1.2 绑定关系原则
+
+| 场景 | 错误做法 | 正确做法 |
+| --- | --- | --- |
+| 新建智能体勾选技能 | 在 Agent 上保存技能 ID 数组 | `AgentSkillBinding` 中间表 |
+| 技能选择 MCP 工具 | 在 Skill 上保存工具 ID 数组 | `SkillToolBinding` 中间表 |
+| 团队选择智能体成员 | 在 Team 上保存成员数组 | `TeamMember` 中间表 |
+| 角色拥有权限 | 在 Role 上保存权限数组 | `RolePermissionBinding` 中间表 |
+
+前端交互可以是“一次提交”,例如创建智能体弹窗里勾选多个技能。API 应提供事务型接口,服务端负责创建主对象和绑定关系,保证绑定表落库。
+
+## 2. 服务划分
+
+| 服务 | 路由前缀 | 数据归属 | 对应界面 | 依赖关系 |
+| --- | --- | --- | --- | --- |
+| `gatewayService` | `/gateway/system` | 聚合、健康检查、统一鉴权转发 | Dashboard、全局错误 | 调用所有服务 |
+| `identityService` | `/gateway/identity` | 用户、角色、权限绑定、API Key | Login、Settings | 无 |
+| `modelService` | `/gateway/model` | 模型配置、模型发现、模型测试 | Models、Agent 创建右侧模型选择、Knowledge 设置 | 可调用外部模型供应商 |
+| `toolService` | `/gateway/tool` | MCP 服务、MCP 内部工具、凭据 | Tools、Skill 工具选择 | 可连接外部 MCP |
+| `skillService` | `/gateway/skill` | 技能、技能工具绑定、技能安装、技能测试 | Skills、Agent 创建技能勾选 | 依赖 toolService |
+| `agentService` | `/gateway/agent` | 智能体、智能体技能绑定、智能体运行 | Agents、Sessions | 依赖 modelService、skillService、memoryService |
+| `teamService` | `/gateway/team` | 团队、团队成员绑定、团队运行 | Teams | 依赖 agentService |
+| `sessionService` | `/gateway/session` | 会话、消息、运行请求 | Sessions | 依赖 agentService、workflowService |
+| `knowledgeService` | `/gateway/knowledge` | 知识库、文档、切片、检索设置、索引任务、评估 | Knowledge | 依赖 modelService |
+| `memoryService` | `/gateway/memory` | 记忆、向量检索、记忆归档 | Memories、Agent 执行 | 依赖 modelService |
+| `workflowService` | `/gateway/workflow` | 应用、工作流、设计器、调试器 | Workflow 设计器、未来多应用入口 | 依赖 agentService、teamService、toolService |
+| `runtimeService` | `/gateway/runtime` | 运行记录、节点运行、日志、Trace | Dashboard、调试器、运行详情 | 由执行侧写入 |
+
+## 3. 页面交互与服务 API 闭环
+
+| 页面 | 关键交互 | 调用服务 | API 闭环 |
+| --- | --- | --- | --- |
+| Login | 登录、获取当前用户、退出 | identityService | `auth/login`、`auth/me`、`auth/logout` |
+| Dashboard | 指标、服务健康、最近运行 | gatewayService、runtimeService | `dashboard/summary`、`health/services`、`runs/list` |
+| Models | 模型列表、新建、编辑、删除、测试、发现模型 | modelService | `models/list`、`models/create`、`models/test`、`models/discover` |
+| Agents | 列表、创建、编辑、技能勾选、运行测试 | agentService、skillService、modelService | `agents/createWithBindings`、`agentSkillBindings/sync`、`agentRuns/start` |
+| Sessions | 会话创建、消息发送、触发运行、查看上下文 | sessionService、agentService | `messages/send` 生成消息和运行请求 |
+| Tools | 粘贴 MCP 配置、测试连接、发现内部工具、查看参数 | toolService | `mcpServers/importConfig`、`mcpServers/test`、`mcpTools/list` |
+| Skills | 技能 CRUD、选择 MCP 工具、测试技能 | skillService、toolService | `skills/createWithBindings`、`skillToolBindings/sync`、`skills/test` |
+| Knowledge | 知识库列表、进入内部、导入文档、检索测试、Rerank 设置 | knowledgeService、modelService | `bases/list`、`documents/create`、`settings/save`、`search/query` |
+| Memories | 只读查看、筛选、语义搜索、详情 | memoryService | `memories/list`、`memories/search`、`memories/get` |
+| Teams | 创建团队、添加成员、运行团队 | teamService、agentService | `teams/createWithMembers`、`teamMembers/sync`、`teamRuns/start` |
+| Settings | API Key 创建和撤销 | identityService | `apiKeys/create`、`apiKeys/revoke` |
+
+## 4. 共享模型
+
+### 4.1 Identity 模型
+
+`User`
+
+| 字段 | 类型 | 必填 | 说明 |
+| --- | --- | --- | --- |
+| `id` | string | 是 | 用户 ID |
+| `username` | string | 是 | 登录名 |
+| `displayName` | string \| null | 否 | 展示名 |
+| `email` | string \| null | 否 | 邮箱 |
+| `metadata` | object | 是 | 扩展信息 |
+| `lastLoginTime` | datetime \| null | 否 | 最近登录时间 |
+| `createdTime` | datetime | 是 | 创建时间 |
+| `updatedTime` | datetime | 是 | 更新时间 |
+
+`Role`
+
+| 字段 | 类型 | 必填 | 说明 |
+| --- | --- | --- | --- |
+| `id` | string | 是 | 角色 ID |
+| `name` | string | 是 | 角色名称 |
+| `description` | string \| null | 否 | 说明 |
+| `permissionBindingCount` | integer | 是 | 权限绑定数量 |
+| `createdTime` | datetime | 是 | 创建时间 |
+| `updatedTime` | datetime | 是 | 更新时间 |
+
+`RolePermissionBinding`
+
+| 字段 | 类型 | 必填 | 说明 |
+| --- | --- | --- | --- |
+| `id` | string | 是 | 绑定 ID |
+| `roleId` | string | 是 | 角色 ID |
+| `permission` | string | 是 | 权限标识 |
+| `scopeType` | string \| null | 否 | 范围类型 |
+| `scopeId` | string \| null | 否 | 范围对象 ID |
+| `createdTime` | datetime | 是 | 创建时间 |
+
+`ApiKey`
+
+| 字段 | 类型 | 必填 | 说明 |
+| --- | --- | --- | --- |
+| `id` | string | 是 | API Key ID |
+| `name` | string | 是 | 名称 |
+| `keyPrefix` | string | 是 | 密钥前缀 |
+| `scopes` | string \| null | 否 | 权限范围 |
+| `expiresTime` | datetime \| null | 否 | 过期时间 |
+| `lastUsedTime` | datetime \| null | 否 | 最近使用时间 |
+| `revokedTime` | datetime \| null | 否 | 撤销时间 |
+| `createdTime` | datetime | 是 | 创建时间 |
+
+### 4.2 Model 模型
+
+`Model`
+
+| 字段 | 类型 | 必填 | 说明 |
+| --- | --- | --- | --- |
+| `id` | string | 是 | 模型配置 ID |
+| `name` | string | 是 | 展示名称 |
+| `providerType` | string | 是 | 供应商类型 |
+| `providerBaseUrl` | string | 是 | 接入地址 |
+| `hasProviderApiKey` | boolean | 是 | 是否已保存密钥 |
+| `modelName` | string | 是 | 供应商模型名 |
+| `capabilities` | string[] | 是 | 能力标签,例如 `chat`、`embedding`、`rerank` |
+| `contextWindow` | integer \| null | 否 | 上下文窗口 |
+| `maxOutputTokens` | integer \| null | 否 | 最大输出 token |
+| `defaultTemperature` | number \| null | 否 | 默认温度 |
+| `timeoutSeconds` | integer | 是 | 超时时间 |
+| `metadata` | object | 否 | 扩展信息 |
+| `createdTime` | datetime | 是 | 创建时间 |
+| `updatedTime` | datetime | 是 | 更新时间 |
+
+`ModelTestResult`
+
+| 字段 | 类型 | 必填 | 说明 |
+| --- | --- | --- | --- |
+| `model` | `Model` | 是 | 被测试模型 |
+| `content` | string | 是 | 文本输出 |
+| `finishReason` | string \| null | 否 | 结束原因 |
+| `toolCalls` | object[] | 否 | 模型返回的工具调用 |
+| `tokenUsage` | object | 是 | token 用量 |
+| `latencyMs` | integer | 是 | 调用耗时 |
+| `testedTime` | datetime | 是 | 测试时间 |
+
+### 4.3 Tool 模型
+
+`McpServer`
+
+| 字段 | 类型 | 必填 | 说明 |
+| --- | --- | --- | --- |
+| `id` | string | 是 | MCP 服务 ID |
+| `name` | string | 是 | 服务名称 |
+| `transport` | `"sse"` \| `"streamableHttp"` \| `"stdio"` | 是 | 连接协议 |
+| `url` | string \| null | 否 | 连接地址 |
+| `headersMasked` | object | 是 | 脱敏请求头 |
+| `timeoutSeconds` | integer | 是 | 连接超时 |
+| `sseReadTimeoutSeconds` | integer \| null | 否 | SSE 读取超时 |
+| `toolCount` | integer | 是 | 已发现工具数量 |
+| `lastTestResult` | `ConnectionTestResult` \| null | 否 | 最近连接测试 |
+| `createdTime` | datetime | 是 | 创建时间 |
+| `updatedTime` | datetime | 是 | 更新时间 |
+
+`McpTool`
+
+| 字段 | 类型 | 必填 | 说明 |
+| --- | --- | --- | --- |
+| `id` | string | 是 | MCP 内部工具 ID |
+| `mcpServerId` | string | 是 | MCP 服务 ID |
+| `name` | string | 是 | 工具名称 |
+| `description` | string \| null | 否 | 工具说明 |
+| `inputSchema` | object | 是 | 参数 schema |
+| `outputSchema` | object | 否 | 输出 schema |
+| `createdTime` | datetime | 是 | 创建时间 |
+| `updatedTime` | datetime | 是 | 更新时间 |
+
+`ConnectionTestResult`
+
+| 字段 | 类型 | 必填 | 说明 |
+| --- | --- | --- | --- |
+| `success` | boolean | 是 | 是否连通 |
+| `message` | string | 是 | 结果说明 |
+| `latencyMs` | integer \| null | 否 | 延迟 |
+| `toolCount` | integer | 是 | 可发现工具数 |
+| `testedTime` | datetime | 是 | 测试时间 |
+
+### 4.4 Skill 模型
+
+`Skill`
+
+| 字段 | 类型 | 必填 | 说明 |
+| --- | --- | --- | --- |
+| `id` | string | 是 | 技能 ID |
+| `name` | string | 是 | 技能名称 |
+| `category` | string | 是 | 分类 |
+| `description` | string \| null | 否 | 技能说明 |
+| `instruction` | string | 是 | 技能指令 |
+| `parameterSchema` | object | 是 | 入参 schema |
+| `outputSchema` | object | 是 | 出参 schema |
+| `toolBindingCount` | integer | 是 | 已绑定 MCP 工具数量 |
+| `metadata` | object | 否 | 扩展信息 |
+| `createdTime` | datetime | 是 | 创建时间 |
+| `updatedTime` | datetime | 是 | 更新时间 |
+
+`SkillToolBinding`
+
+| 字段 | 类型 | 必填 | 说明 |
+| --- | --- | --- | --- |
+| `id` | string | 是 | 技能工具绑定 ID |
+| `skillId` | string | 是 | 技能 ID |
+| `toolId` | string | 是 | MCP 工具 ID |
+| `orderIndex` | integer | 是 | 展示和调用顺序 |
+| `parameterMapping` | object | 是 | 技能参数到工具参数的映射 |
+| `config` | object | 是 | 局部配置 |
+| `createdTime` | datetime | 是 | 创建时间 |
+| `updatedTime` | datetime | 是 | 更新时间 |
+
+`SkillInstallation`
+
+| 字段 | 类型 | 必填 | 说明 |
+| --- | --- | --- | --- |
+| `id` | string | 是 | 安装 ID |
+| `skillId` | string | 是 | 技能 ID |
+| `installScope` | `"global"` \| `"user"` \| `"agent"` \| `"team"` | 是 | 安装范围 |
+| `scopeId` | string \| null | 否 | 范围对象 ID |
+| `config` | object | 是 | 安装配置 |
+| `installedBy` | string \| null | 否 | 安装人 ID |
+| `installedTime` | datetime | 是 | 安装时间 |
+| `createdTime` | datetime | 是 | 创建时间 |
+
+### 4.5 Agent 模型
+
+`Agent`
+
+| 字段 | 类型 | 必填 | 说明 |
+| --- | --- | --- | --- |
+| `id` | string | 是 | 智能体 ID |
+| `name` | string | 是 | 名称 |
+| `ownerUserId` | string \| null | 否 | 创建人 ID |
+| `modelId` | string \| null | 否 | 绑定模型 ID |
+| `systemPrompt` | string | 是 | 系统提示词 |
+| `skillBindingCount` | integer | 是 | 已绑定技能数量 |
+| `memoryPolicy` | `AgentMemoryPolicy` | 是 | 记忆策略 |
+| `runtimePolicy` | `AgentRuntimePolicy` | 是 | 运行策略 |
+| `metadata` | object | 否 | 扩展信息 |
+| `createdTime` | datetime | 是 | 创建时间 |
+| `updatedTime` | datetime | 是 | 更新时间 |
+
+`AgentSkillBinding`
+
+| 字段 | 类型 | 必填 | 说明 |
+| --- | --- | --- | --- |
+| `id` | string | 是 | 智能体技能绑定 ID |
+| `agentId` | string | 是 | 智能体 ID |
+| `skillId` | string | 是 | 技能 ID |
+| `orderIndex` | integer | 是 | 展示和调用顺序 |
+| `config` | object | 是 | 局部配置 |
+| `createdTime` | datetime | 是 | 创建时间 |
+| `updatedTime` | datetime | 是 | 更新时间 |
+
+`AgentMemoryPolicy`
+
+| 字段 | 类型 | 必填 | 说明 |
+| --- | --- | --- | --- |
+| `memoryScope` | `"session"` \| `"user"` \| `"agent"` \| `"team"` \| `"global"` | 是 | 记忆作用域 |
+| `readMemory` | boolean | 是 | 是否读取记忆 |
+| `writeMemory` | boolean | 是 | 是否写入记忆 |
+| `maxItems` | integer | 是 | 最大召回数量 |
+| `minScore` | number | 是 | 最低召回分数 |
+
+`AgentRuntimePolicy`
+
+| 字段 | 类型 | 必填 | 说明 |
+| --- | --- | --- | --- |
+| `temperature` | number | 是 | 温度 |
+| `maxTokens` | integer | 是 | 最大输出 token |
+| `timeoutSeconds` | integer | 是 | 总超时 |
+| `retryAttempts` | integer | 是 | 重试次数 |
+| `retryBackoffMs` | integer | 是 | 重试退避毫秒 |
+| `toolCallLimit` | integer | 是 | 单次运行最大工具调用次数 |
+| `outputFormat` | `"text"` \| `"json"` \| `"markdown"` | 是 | 输出格式 |
+| `humanApprovalPolicy` | `"never"` \| `"beforeTool"` \| `"beforeFinal"` | 是 | 人工审批策略 |
+
+`AgentRun`
+
+| 字段 | 类型 | 必填 | 说明 |
+| --- | --- | --- | --- |
+| `id` | string | 是 | 运行 ID |
+| `agentId` | string | 是 | 智能体 ID |
+| `sessionId` | string \| null | 否 | 会话 ID |
+| `inputText` | string \| null | 否 | 输入文本 |
+| `input` | object \| null | 否 | 结构化输入 |
+| `outputText` | string \| null | 否 | 输出文本 |
+| `output` | object \| null | 否 | 结构化输出 |
+| `status` | `"queued"` \| `"running"` \| `"completed"` \| `"failed"` \| `"cancelled"` \| `"paused"` | 是 | 运行状态 |
+| `toolCallCount` | integer | 是 | 工具调用次数 |
+| `errorMessage` | string \| null | 否 | 错误消息 |
+| `queuedTime` | datetime \| null | 否 | 排队时间 |
+| `startedTime` | datetime \| null | 否 | 开始时间 |
+| `finishedTime` | datetime \| null | 否 | 结束时间 |
+| `createdTime` | datetime | 是 | 创建时间 |
+
+### 4.6 Session 模型
+
+`Session`
+
+| 字段 | 类型 | 必填 | 说明 |
+| --- | --- | --- | --- |
+| `id` | string | 是 | 会话 ID |
+| `appId` | string | 是 | 应用 ID |
+| `userId` | string | 是 | 用户 ID |
+| `channelType` | string | 是 | 渠道 |
+| `title` | string \| null | 否 | 标题 |
+| `startedTime` | datetime \| null | 否 | 开始时间 |
+| `lastActiveTime` | datetime \| null | 否 | 最近活跃时间 |
+| `createdTime` | datetime | 是 | 创建时间 |
+
+`Message`
+
+| 字段 | 类型 | 必填 | 说明 |
+| --- | --- | --- | --- |
+| `id` | string | 是 | 消息 ID |
+| `sessionId` | string | 是 | 会话 ID |
+| `turnId` | string \| null | 否 | 轮次 ID |
+| `role` | `"user"` \| `"assistant"` \| `"system"` \| `"tool"` | 是 | 角色 |
+| `contentType` | `"text"` \| `"markdown"` \| `"image"` \| `"file"` \| `"object"` | 是 | 内容类型 |
+| `contentText` | string \| null | 否 | 文本内容 |
+| `content` | object \| null | 否 | 结构化内容 |
+| `createdTime` | datetime | 是 | 创建时间 |
+
+`RunRequest`
+
+| 字段 | 类型 | 必填 | 说明 |
+| --- | --- | --- | --- |
+| `id` | string | 是 | 请求 ID |
+| `sessionId` | string | 是 | 会话 ID |
+| `appId` | string | 是 | 应用 ID |
+| `workflowId` | string \| null | 否 | 工作流 ID |
+| `agentId` | string \| null | 否 | 智能体 ID |
+| `triggerType` | string | 是 | 触发方式 |
+| `payload` | object | 是 | 负载 |
+| `status` | `"queued"` \| `"running"` \| `"completed"` \| `"failed"` \| `"cancelled"` | 是 | 状态 |
+| `createdTime` | datetime | 是 | 创建时间 |
+
+### 4.7 Knowledge 模型
+
+`KnowledgeBase`
+
+| 字段 | 类型 | 必填 | 说明 |
+| --- | --- | --- | --- |
+| `id` | string | 是 | 知识库 ID |
+| `name` | string | 是 | 名称 |
+| `description` | string \| null | 否 | 说明 |
+| `documentCount` | integer | 是 | 文档数 |
+| `indexedDocumentCount` | integer | 是 | 已索引文档数 |
+| `chunkCount` | integer | 是 | 切片数 |
+| `settings` | `KnowledgeSettings` | 是 | 检索设置 |
+| `metadata` | object | 否 | 扩展信息 |
+| `archivedTime` | datetime \| null | 否 | 归档时间 |
+| `createdTime` | datetime | 是 | 创建时间 |
+| `updatedTime` | datetime | 是 | 更新时间 |
+
+`KnowledgeSettings`
+
+| 字段 | 类型 | 必填 | 说明 |
+| --- | --- | --- | --- |
+| `retrievalMode` | `"keyword"` \| `"vector"` \| `"hybrid"` | 是 | 检索模式 |
+| `embeddingModelId` | string \| null | 否 | Embedding 模型 ID |
+| `rerankModelId` | string \| null | 否 | Rerank 模型 ID |
+| `chunkSize` | integer | 是 | 切片大小 |
+| `chunkOverlap` | integer | 是 | 切片重叠 |
+| `topK` | integer | 是 | 默认返回数量 |
+| `minScore` | number | 是 | 最低分数 |
+| `maxCandidates` | integer | 是 | 候选数量 |
+| `keywordWeight` | number | 是 | 关键词权重 |
+| `vectorWeight` | number | 是 | 向量权重 |
+| `rerankWeight` | number | 是 | Rerank 权重 |
+| `queryRewrite` | boolean | 是 | 是否查询改写 |
+| `requireCitations` | boolean | 是 | 是否要求引用 |
+
+`KnowledgeDocument`
+
+| 字段 | 类型 | 必填 | 说明 |
+| --- | --- | --- | --- |
+| `id` | string | 是 | 文档 ID |
+| `knowledgeBaseId` | string | 是 | 知识库 ID |
+| `title` | string | 是 | 标题 |
+| `sourceType` | `"text"` \| `"markdown"` \| `"json"` \| `"html"` \| `"pdf"` \| `"docx"` \| `"url"` | 是 | 来源类型 |
+| `sourceUri` | string \| null | 否 | 来源地址 |
+| `indexStatus` | `"draft"` \| `"queued"` \| `"indexed"` \| `"failed"` \| `"archived"` | 是 | 索引状态 |
+| `contentHash` | string \| null | 否 | 内容 hash |
+| `metadata` | object | 否 | 扩展信息 |
+| `indexedTime` | datetime \| null | 否 | 索引完成时间 |
+| `createdTime` | datetime | 是 | 创建时间 |
+| `updatedTime` | datetime | 是 | 更新时间 |
+
+`KnowledgeChunk`
+
+| 字段 | 类型 | 必填 | 说明 |
+| --- | --- | --- | --- |
+| `id` | string | 是 | 切片 ID |
+| `knowledgeBaseId` | string | 是 | 知识库 ID |
+| `documentId` | string | 是 | 文档 ID |
+| `chunkIndex` | integer | 是 | 切片序号 |
+| `contentText` | string | 是 | 切片内容 |
+| `tokenCount` | integer | 是 | token 数 |
+| `embeddingModelId` | string \| null | 否 | 向量模型 ID |
+| `metadata` | object | 否 | 扩展信息 |
+| `createdTime` | datetime | 是 | 创建时间 |
+
+### 4.8 Memory 模型
+
+`MemoryItem`
+
+| 字段 | 类型 | 必填 | 说明 |
+| --- | --- | --- | --- |
+| `id` | string | 是 | 记忆 ID |
+| `scopeType` | `"global"` \| `"user"` \| `"session"` \| `"agent"` \| `"team"` | 是 | 作用域类型 |
+| `scopeId` | string | 是 | 作用域对象 ID |
+| `memoryType` | string | 是 | 记忆类型 |
+| `contentText` | string | 是 | 记忆内容 |
+| `content` | object \| null | 否 | 结构化内容 |
+| `metadata` | object | 是 | 扩展信息 |
+| `embeddingModelId` | string \| null | 否 | Embedding 模型 ID |
+| `ownerAgentId` | string \| null | 否 | 归属智能体 ID |
+| `userId` | string \| null | 否 | 用户 ID |
+| `sessionId` | string \| null | 否 | 会话 ID |
+| `sourceRef` | string \| null | 否 | 来源引用 |
+| `importanceScore` | number | 是 | 重要度 |
+| `lastAccessedTime` | datetime \| null | 否 | 最近访问时间 |
+| `expiresTime` | datetime \| null | 否 | 过期时间 |
+| `archivedTime` | datetime \| null | 否 | 归档时间 |
+| `createdTime` | datetime | 是 | 创建时间 |
+| `updatedTime` | datetime | 是 | 更新时间 |
+
+### 4.9 Team 模型
+
+`Team`
+
+| 字段 | 类型 | 必填 | 说明 |
+| --- | --- | --- | --- |
+| `id` | string | 是 | 团队 ID |
+| `name` | string | 是 | 名称 |
+| `description` | string \| null | 否 | 说明 |
+| `ownerUserId` | string \| null | 否 | 创建人 ID |
+| `coordinationMode` | `"supervisor"` \| `"collaborative"` \| `"sequential"` \| `"debate"` | 是 | 协作模式 |
+| `objective` | string \| null | 否 | 目标 |
+| `memberCount` | integer | 是 | 成员数量 |
+| `policy` | `TeamPolicy` | 是 | 团队策略 |
+| `metadata` | object | 否 | 扩展信息 |
+| `createdTime` | datetime | 是 | 创建时间 |
+| `updatedTime` | datetime | 是 | 更新时间 |
+
+`TeamMember`
+
+| 字段 | 类型 | 必填 | 说明 |
+| --- | --- | --- | --- |
+| `id` | string | 是 | 成员绑定 ID |
+| `teamId` | string | 是 | 团队 ID |
+| `agentId` | string | 是 | 智能体 ID |
+| `role` | string | 是 | 团队内角色 |
+| `responsibility` | string \| null | 否 | 职责说明 |
+| `orderIndex` | integer | 是 | 顺序 |
+| `config` | object | 是 | 局部配置 |
+| `createdTime` | datetime | 是 | 创建时间 |
+| `updatedTime` | datetime | 是 | 更新时间 |
+
+`TeamPolicy`
+
+| 字段 | 类型 | 必填 | 说明 |
+| --- | --- | --- | --- |
+| `maxRounds` | integer | 是 | 最大轮次 |
+| `handoff` | `"supervisor"` \| `"roundRobin"` \| `"auto"` | 是 | 交接策略 |
+| `failureMode` | `"stopOnCritical"` \| `"continue"` \| `"fallback"` | 是 | 失败策略 |
+| `timeoutSeconds` | integer | 是 | 总超时 |
+| `humanApprovalPolicy` | `"never"` \| `"beforeFinal"` \| `"onRisk"` | 是 | 人工审批策略 |
+
+`TeamRun`
+
+| 字段 | 类型 | 必填 | 说明 |
+| --- | --- | --- | --- |
+| `id` | string | 是 | 团队运行 ID |
+| `teamId` | string | 是 | 团队 ID |
+| `sessionId` | string \| null | 否 | 会话 ID |
+| `inputText` | string \| null | 否 | 输入文本 |
+| `input` | object \| null | 否 | 结构化输入 |
+| `outputText` | string \| null | 否 | 输出文本 |
+| `output` | object \| null | 否 | 结构化输出 |
+| `status` | `"queued"` \| `"running"` \| `"completed"` \| `"failed"` \| `"cancelled"` \| `"paused"` | 是 | 状态 |
+| `createdTime` | datetime | 是 | 创建时间 |
+| `startedTime` | datetime \| null | 否 | 开始时间 |
+| `finishedTime` | datetime \| null | 否 | 结束时间 |
+
+## 5. 按服务划分的 API
+
+下方所有接口都是 `POST`。路径中的服务前缀代表网关转发目标。
+
+### 5.1 gatewayService
+
+| 接口 | 输入参数 | 输出 | 说明 |
+| --- | --- | --- | --- |
+| `POST /gateway/system/dashboard/summary` | `timeRange:"24h"|"7d"|"30d"` 默认 `7d` | `agentCount:integer`;`sessionCount:integer`;`runCount:integer`;`failedRunCount:integer`;`liveRunCount:integer`;`healthyServiceCount:integer`;`trend:Array<{date:string,total:integer,successful:integer,failed:integer}>` | 仪表盘聚合 |
+| `POST /gateway/system/health/get` | 无 | `service:string`;`status:string`;`database:string|null`;`checkedTime:datetime` | 网关健康 |
+| `POST /gateway/system/health/services` | 无 | `service:string`;`status:string`;`downstreamServices:Array<{service:string,status:string,url:string,httpStatus:integer|null,errorMessage:string|null}>` | 下游服务健康 |
+
+### 5.2 identityService
+
+| 接口 | 输入参数 | 输出 | 说明 |
+| --- | --- | --- | --- |
+| `POST /gateway/identity/auth/login` | `username:string` 必填;`password:string` 必填 | `accessToken:string`;`tokenType:"bearer"`;`expiresTime:datetime`;`user:User` | 登录 |
+| `POST /gateway/identity/auth/logout` | 无 | `ok:boolean` | 退出登录 |
+| `POST /gateway/identity/auth/me` | 无 | `user:User`;`roles:Role[]`;`permissions:string[]` | 当前用户信息 |
+| `POST /gateway/identity/users/list` | `PageRequest` | `PageResult<User>` | 用户列表 |
+| `POST /gateway/identity/roles/list` | `PageRequest` | `PageResult<Role>` | 角色列表 |
+| `POST /gateway/identity/rolePermissionBindings/list` | `PageRequest`;`roleId:string` 必填 | `PageResult<RolePermissionBinding>` | 角色权限绑定列表 |
+| `POST /gateway/identity/rolePermissionBindings/add` | `roleId:string` 必填;`permission:string` 必填;`scopeType:string|null`;`scopeId:string|null` | `RolePermissionBinding` | 新增角色权限绑定 |
+| `POST /gateway/identity/rolePermissionBindings/remove` | `bindingId:string` 必填 | `deleted:boolean`;`bindingId:string` | 删除角色权限绑定 |
+| `POST /gateway/identity/permissions/check` | `userId:string` 必填;`permission:string` 必填;`scopeType:string|null`;`scopeId:string|null` | `allowed:boolean`;`reason:string`;`matchedRoleIds:string[]` | 权限检查 |
+| `POST /gateway/identity/apiKeys/list` | `PageRequest` | `PageResult<ApiKey>` | API Key 列表 |
+| `POST /gateway/identity/apiKeys/create` | `name:string` 必填;`scopes:string|null`;`expiresTime:datetime|null` | `apiKey:ApiKey`;`secret:string` | 创建 API Key,明文只返回一次 |
+| `POST /gateway/identity/apiKeys/revoke` | `apiKeyId:string` 必填 | `ApiKey` | 撤销 API Key |
+
+### 5.3 modelService
+
+| 接口 | 输入参数 | 输出 | 说明 |
+| --- | --- | --- | --- |
+| `POST /gateway/model/models/list` | `PageRequest`;`providerType:string|null`;`capability:string|null` | `PageResult<Model>` | 模型列表 |
+| `POST /gateway/model/models/get` | `modelId:string` 必填 | `Model` | 模型详情 |
+| `POST /gateway/model/models/create` | `name:string` 必填;`providerType:string` 必填;`providerBaseUrl:string` 必填;`providerApiKey:string|null`;`modelName:string` 必填;`capabilities:string[]`;`contextWindow:integer|null`;`maxOutputTokens:integer|null`;`defaultTemperature:number|null`;`timeoutSeconds:integer`;`metadata:object` | `Model` | 新建模型 |
+| `POST /gateway/model/models/update` | `modelId:string` 必填;其余字段同 create,均可选 | `Model` | 更新模型 |
+| `POST /gateway/model/models/delete` | `modelId:string` 必填 | `deleted:boolean`;`modelId:string` | 删除模型 |
+| `POST /gateway/model/models/test` | `modelId:string` 必填;`prompt:string` 必填;`systemPrompt:string|null`;`temperature:number|null`;`maxTokens:integer|null` | `ModelTestResult` | 测试模型 |
+| `POST /gateway/model/models/discover` | `providerType:string` 必填;`providerBaseUrl:string` 必填;`providerApiKey:string|null` | `models:Array<{modelName:string,displayName:string,capabilities:string[],contextWindow:integer|null}>` | 自动发现模型 |
+
+### 5.4 toolService
+
+| 接口 | 输入参数 | 输出 | 说明 |
+| --- | --- | --- | --- |
+| `POST /gateway/tool/mcpServers/list` | `PageRequest` | `PageResult<McpServer>` | MCP 服务列表 |
+| `POST /gateway/tool/mcpServers/get` | `mcpServerId:string` 必填 | `server:McpServer`;`tools:McpTool[]` | MCP 服务详情 |
+| `POST /gateway/tool/mcpServers/importConfig` | `config:object` 必填;`testConnection:boolean` 默认 `true`;`discoverTools:boolean` 默认 `true` | `servers:McpServer[]`;`testResults:ConnectionTestResult[]`;`discoveredTools:McpTool[]` | 支持粘贴 MCP 配置 |
+| `POST /gateway/tool/mcpServers/create` | `name:string` 必填;`transport:string` 默认 `sse`;`url:string|null`;`headers:object`;`timeoutSeconds:integer`;`sseReadTimeoutSeconds:integer|null` | `McpServer` | 创建 MCP 服务 |
+| `POST /gateway/tool/mcpServers/update` | `mcpServerId:string` 必填;其余字段同 create,均可选 | `McpServer` | 更新 MCP 服务 |
+| `POST /gateway/tool/mcpServers/delete` | `mcpServerId:string` 必填 | `deleted:boolean`;`mcpServerId:string` | 删除 MCP 服务 |
+| `POST /gateway/tool/mcpServers/test` | `mcpServerId:string|null`;`name:string|null`;`transport:string|null`;`url:string|null`;`headers:object|null`;`timeoutSeconds:integer|null`;`sseReadTimeoutSeconds:integer|null` | `ConnectionTestResult` | 测试连接 |
+| `POST /gateway/tool/mcpServers/discoverTools` | `mcpServerId:string` 必填;`refresh:boolean` 默认 `true` | `mcpServerId:string`;`tools:McpTool[]`;`discoveredTime:datetime` | 发现 MCP 内部工具 |
+| `POST /gateway/tool/mcpTools/list` | `PageRequest`;`mcpServerId:string|null` | `PageResult<McpTool>` | MCP 内部工具列表 |
+| `POST /gateway/tool/mcpTools/get` | `toolId:string` 必填 | `McpTool` | MCP 内部工具详情 |
+
+### 5.5 skillService
+
+| 接口 | 输入参数 | 输出 | 说明 |
+| --- | --- | --- | --- |
+| `POST /gateway/skill/skills/list` | `PageRequest`;`category:string|null`;`toolId:string|null` | `PageResult<Skill>` | 技能列表 |
+| `POST /gateway/skill/skills/get` | `skillId:string` 必填 | `skill:Skill`;`toolBindings:SkillToolBinding[]` | 技能详情 |
+| `POST /gateway/skill/skills/create` | `name:string` 必填;`category:string` 必填;`description:string|null`;`instruction:string` 必填;`parameterSchema:object`;`outputSchema:object`;`metadata:object` | `Skill` | 创建技能 |
+| `POST /gateway/skill/skills/createWithBindings` | `skill:SkillCreateInput` 必填;`toolBindings:Array<{toolId:string,orderIndex:integer,parameterMapping:object,config:object}>` | `skill:Skill`;`toolBindings:SkillToolBinding[]` | 创建技能并写入工具绑定表 |
+| `POST /gateway/skill/skills/update` | `skillId:string` 必填;其余字段同 create,均可选 | `Skill` | 更新技能 |
+| `POST /gateway/skill/skills/delete` | `skillId:string` 必填 | `deleted:boolean`;`skillId:string` | 删除技能 |
+| `POST /gateway/skill/skillToolBindings/list` | `PageRequest`;`skillId:string` 必填 | `PageResult<SkillToolBinding>` | 技能工具绑定列表 |
+| `POST /gateway/skill/skillToolBindings/add` | `skillId:string` 必填;`toolId:string` 必填;`orderIndex:integer|null`;`parameterMapping:object`;`config:object` | `SkillToolBinding` | 新增技能工具绑定 |
+| `POST /gateway/skill/skillToolBindings/update` | `bindingId:string` 必填;`orderIndex:integer|null`;`parameterMapping:object|null`;`config:object|null` | `SkillToolBinding` | 更新技能工具绑定 |
+| `POST /gateway/skill/skillToolBindings/remove` | `bindingId:string` 必填 | `deleted:boolean`;`bindingId:string` | 删除技能工具绑定 |
+| `POST /gateway/skill/skillToolBindings/sync` | `skillId:string` 必填;`toolBindings:Array<{toolId:string,orderIndex:integer,parameterMapping:object,config:object}>` | `skill:Skill`;`toolBindings:SkillToolBinding[]` | 用当前勾选结果同步中间表 |
+| `POST /gateway/skill/skillInstallations/list` | `PageRequest`;`skillId:string|null`;`installScope:string|null`;`scopeId:string|null` | `PageResult<SkillInstallation>` | 技能安装列表 |
+| `POST /gateway/skill/skillInstallations/install` | `skillId:string` 必填;`installScope:string` 必填;`scopeId:string|null`;`config:object` | `SkillInstallation` | 安装技能 |
+| `POST /gateway/skill/skillInstallations/uninstall` | `installationId:string` 必填 | `deleted:boolean`;`installationId:string` | 卸载技能 |
+| `POST /gateway/skill/skills/test` | `skillId:string` 必填;`input:object` 必填;`agentId:string|null`;`sessionId:string|null` | `output:object`;`logs:object[]`;`latencyMs:integer` | 测试技能 |
+
+`SkillCreateInput`
+
+| 字段 | 类型 | 必填 | 说明 |
+| --- | --- | --- | --- |
+| `name` | string | 是 | 技能名称 |
+| `category` | string | 是 | 分类 |
+| `description` | string \| null | 否 | 说明 |
+| `instruction` | string | 是 | 技能指令 |
+| `parameterSchema` | object | 是 | 入参 schema |
+| `outputSchema` | object | 是 | 出参 schema |
+| `metadata` | object | 否 | 扩展信息 |
+
+### 5.6 agentService
+
+| 接口 | 输入参数 | 输出 | 说明 |
+| --- | --- | --- | --- |
+| `POST /gateway/agent/agents/list` | `PageRequest`;`ownerUserId:string|null`;`skillId:string|null`;`modelId:string|null` | `PageResult<Agent>` | 智能体列表 |
+| `POST /gateway/agent/agents/get` | `agentId:string` 必填 | `agent:Agent`;`skillBindings:AgentSkillBinding[]` | 智能体详情 |
+| `POST /gateway/agent/agents/create` | `name:string` 必填;`ownerUserId:string|null`;`modelId:string|null`;`systemPrompt:string`;`memoryPolicy:AgentMemoryPolicy`;`runtimePolicy:AgentRuntimePolicy`;`metadata:object` | `Agent` | 创建智能体,不包含技能数组 |
+| `POST /gateway/agent/agents/createWithBindings` | `agent:AgentCreateInput` 必填;`skillBindings:Array<{skillId:string,orderIndex:integer,config:object}>` | `agent:Agent`;`skillBindings:AgentSkillBinding[]` | 创建智能体并写入技能绑定表,适配新建弹窗勾选技能 |
+| `POST /gateway/agent/agents/update` | `agentId:string` 必填;其余字段同 create,均可选 | `Agent` | 更新智能体基础配置 |
+| `POST /gateway/agent/agents/delete` | `agentId:string` 必填;`deleteRuns:boolean` 默认 `false` | `deleted:boolean`;`agentId:string` | 删除智能体 |
+| `POST /gateway/agent/agentSkillBindings/list` | `PageRequest`;`agentId:string` 必填 | `PageResult<AgentSkillBinding>` | 智能体技能绑定列表 |
+| `POST /gateway/agent/agentSkillBindings/add` | `agentId:string` 必填;`skillId:string` 必填;`orderIndex:integer|null`;`config:object` | `AgentSkillBinding` | 新增智能体技能绑定 |
+| `POST /gateway/agent/agentSkillBindings/update` | `bindingId:string` 必填;`orderIndex:integer|null`;`config:object|null` | `AgentSkillBinding` | 更新智能体技能绑定 |
+| `POST /gateway/agent/agentSkillBindings/remove` | `bindingId:string` 必填 | `deleted:boolean`;`bindingId:string` | 删除智能体技能绑定 |
+| `POST /gateway/agent/agentSkillBindings/sync` | `agentId:string` 必填;`skillBindings:Array<{skillId:string,orderIndex:integer,config:object}>` | `agent:Agent`;`skillBindings:AgentSkillBinding[]` | 用当前勾选结果同步中间表 |
+| `POST /gateway/agent/agentRuns/list` | `PageRequest`;`agentId:string|null`;`sessionId:string|null`;`status:string|null`;`startTime:datetime|null`;`endTime:datetime|null` | `PageResult<AgentRun>` | 运行历史 |
+| `POST /gateway/agent/agentRuns/start` | `agentId:string` 必填;`sessionId:string|null`;`inputText:string|null`;`input:object|null`;`stream:boolean` 默认 `false` | `AgentRun` | 启动智能体运行 |
+| `POST /gateway/agent/agentRuns/get` | `runId:string` 必填 | `AgentRun` | 运行详情 |
+| `POST /gateway/agent/agentRuns/poll` | `runId:string` 必填;`afterTime:datetime|null` | `run:AgentRun`;`messages:Message[]`;`logs:object[]`;`toolCalls:Array<{id:string,skillId:string|null,toolId:string|null,name:string,input:object,output:object|null,status:string,startedTime:datetime|null,finishedTime:datetime|null}>` | 轮询运行进度 |
+| `POST /gateway/agent/agentRuns/cancel` | `runId:string` 必填;`reason:string|null` | `AgentRun` | 取消运行 |
+
+`AgentCreateInput`
+
+| 字段 | 类型 | 必填 | 说明 |
+| --- | --- | --- | --- |
+| `name` | string | 是 | 智能体名称 |
+| `ownerUserId` | string \| null | 否 | 创建人 ID |
+| `modelId` | string \| null | 否 | 模型 ID |
+| `systemPrompt` | string | 是 | 系统提示词 |
+| `memoryPolicy` | `AgentMemoryPolicy` | 是 | 记忆策略 |
+| `runtimePolicy` | `AgentRuntimePolicy` | 是 | 运行策略 |
+| `metadata` | object | 否 | 扩展信息 |
+
+### 5.7 sessionService
+
+| 接口 | 输入参数 | 输出 | 说明 |
+| --- | --- | --- | --- |
+| `POST /gateway/session/sessions/list` | `PageRequest`;`appId:string|null`;`userId:string|null`;`channelType:string|null` | `PageResult<Session>` | 会话列表 |
+| `POST /gateway/session/sessions/get` | `sessionId:string` 必填 | `Session` | 会话详情 |
+| `POST /gateway/session/sessions/create` | `appId:string` 必填;`userId:string` 必填;`channelType:string` 默认 `web`;`title:string|null` | `Session` | 创建会话 |
+| `POST /gateway/session/messages/list` | `PageRequest`;`sessionId:string` 必填;`afterTime:datetime|null` | `PageResult<Message>` | 消息列表 |
+| `POST /gateway/session/messages/send` | `sessionId:string` 必填;`contentText:string` 必填;`contentType:"text"|"markdown"` 默认 `text`;`agentId:string|null`;`workflowId:string|null`;`triggerRun:boolean` 默认 `true` | `message:Message`;`runRequest:RunRequest|null` | 发送消息并可触发运行 |
+| `POST /gateway/session/runRequests/list` | `PageRequest`;`sessionId:string` 必填 | `PageResult<RunRequest>` | 会话运行请求 |
+| `POST /gateway/session/context/get` | `sessionId:string` 必填 | `session:Session`;`messageCount:integer`;`runCount:integer`;`recentRunRequests:RunRequest[]` | 会话上下文 |
+
+### 5.8 knowledgeService
+
+| 接口 | 输入参数 | 输出 | 说明 |
+| --- | --- | --- | --- |
+| `POST /gateway/knowledge/bases/list` | `PageRequest`;`includeArchived:boolean` 默认 `false` | `PageResult<KnowledgeBase>` | 知识库列表 |
+| `POST /gateway/knowledge/bases/get` | `knowledgeBaseId:string` 必填 | `KnowledgeBase` | 知识库详情 |
+| `POST /gateway/knowledge/bases/create` | `name:string` 必填;`description:string|null`;`settings:KnowledgeSettings|null`;`metadata:object` | `KnowledgeBase` | 创建知识库 |
+| `POST /gateway/knowledge/bases/update` | `knowledgeBaseId:string` 必填;`name:string|null`;`description:string|null`;`metadata:object|null` | `KnowledgeBase` | 更新知识库 |
+| `POST /gateway/knowledge/bases/archive` | `knowledgeBaseId:string` 必填 | `KnowledgeBase` | 归档知识库 |
+| `POST /gateway/knowledge/bases/restore` | `knowledgeBaseId:string` 必填 | `KnowledgeBase` | 恢复知识库 |
+| `POST /gateway/knowledge/settings/get` | `knowledgeBaseId:string` 必填 | `KnowledgeSettings` | 获取检索设置 |
+| `POST /gateway/knowledge/settings/save` | `knowledgeBaseId:string` 必填;`settings:KnowledgeSettings` 必填 | `KnowledgeSettings` | 保存检索设置 |
+| `POST /gateway/knowledge/documents/list` | `PageRequest`;`knowledgeBaseId:string` 必填;`sourceType:string|null`;`indexStatus:string|null` | `PageResult<KnowledgeDocument>` | 文档列表 |
+| `POST /gateway/knowledge/documents/parse` | `sourceType:string` 必填;`sourceUri:string|null`;`contentText:string|null`;`contentBase64:string|null`;`fileName:string|null` | `contentText:string`;`sourceType:string`;`metadata:object` | 文档解析预览 |
+| `POST /gateway/knowledge/documents/create` | `knowledgeBaseId:string` 必填;`title:string` 必填;`sourceType:string` 必填;`sourceUri:string|null`;`contentText:string|null`;`contentBase64:string|null`;`metadata:object`;`chunkSize:integer|null`;`chunkOverlap:integer|null` | `document:KnowledgeDocument`;`chunks:KnowledgeChunk[]`;`job:object|null` | 导入文档 |
+| `POST /gateway/knowledge/documents/delete` | `documentId:string` 必填 | `deleted:boolean`;`documentId:string` | 删除文档 |
+| `POST /gateway/knowledge/documents/reindex` | `documentId:string` 必填;`settings:KnowledgeSettings|null` | `job:object` | 重建索引 |
+| `POST /gateway/knowledge/search/query` | `knowledgeBaseId:string` 必填;`query:string` 必填;`topK:integer` 默认 5;`filters:object`;`rerankModelId:string|null`;`includeScoreDetail:boolean` 默认 `true` | `items:Array<{chunk:KnowledgeChunk,document:KnowledgeDocument,score:number,scoreDetail:object,citation:object|null}>` | 检索测试 |
+| `POST /gateway/knowledge/jobs/list` | `PageRequest`;`knowledgeBaseId:string|null`;`documentId:string|null`;`status:string|null` | `PageResult<object>` | 索引任务 |
+| `POST /gateway/knowledge/jobs/create` | `knowledgeBaseId:string` 必填;`documentId:string|null`;`jobType:string` 必填;`payload:object` | `job:object` | 创建任务 |
+| `POST /gateway/knowledge/jobs/retry` | `jobId:string` 必填 | `job:object` | 重试任务 |
+| `POST /gateway/knowledge/jobs/cancel` | `jobId:string` 必填;`reason:string|null` | `job:object` | 取消任务 |
+| `POST /gateway/knowledge/evals/list` | `PageRequest`;`knowledgeBaseId:string` 必填 | `PageResult<object>` | 评估集 |
+| `POST /gateway/knowledge/evals/create` | `knowledgeBaseId:string` 必填;`query:string` 必填;`expected:string` 必填 | `item:object` | 新增评估问题 |
+| `POST /gateway/knowledge/evals/run` | `knowledgeBaseId:string` 必填;`evalIds:string[]|null` | `job:object` | 运行评估 |
+
+### 5.9 memoryService
+
+| 接口 | 输入参数 | 输出 | 说明 |
+| --- | --- | --- | --- |
+| `POST /gateway/memory/memories/list` | `PageRequest`;`scopeType:string|null`;`scopeId:string|null`;`memoryType:string|null`;`ownerAgentId:string|null`;`userId:string|null`;`sessionId:string|null` | `PageResult<MemoryItem>` | 记忆列表 |
+| `POST /gateway/memory/memories/get` | `memoryId:string` 必填 | `MemoryItem` | 记忆详情 |
+| `POST /gateway/memory/memories/search` | `query:string` 必填;`scopeType:string|null`;`scopeId:string|null`;`ownerAgentId:string|null`;`userId:string|null`;`sessionId:string|null`;`limit:integer` 默认 10 | `items:Array<{item:MemoryItem,score:number,scoreDetail:object}>` | 语义搜索 |
+| `POST /gateway/memory/memories/create` | `scopeType:string` 必填;`scopeId:string` 必填;`memoryType:string` 必填;`contentText:string` 必填;`content:object|null`;`metadata:object`;`ownerAgentId:string|null`;`userId:string|null`;`sessionId:string|null`;`sourceRef:string|null`;`importanceScore:number`;`expiresTime:datetime|null` | `MemoryItem` | 系统内部写入记忆 |
+| `POST /gateway/memory/memories/archive` | `memoryId:string` 必填 | `MemoryItem` | 归档记忆 |
+| `POST /gateway/memory/memories/delete` | `memoryId:string` 必填 | `deleted:boolean`;`memoryId:string` | 删除记忆 |
+
+### 5.10 teamService
+
+| 接口 | 输入参数 | 输出 | 说明 |
+| --- | --- | --- | --- |
+| `POST /gateway/team/teams/list` | `PageRequest`;`ownerUserId:string|null`;`agentId:string|null` | `PageResult<Team>` | 团队列表 |
+| `POST /gateway/team/teams/get` | `teamId:string` 必填 | `team:Team`;`members:TeamMember[]` | 团队详情 |
+| `POST /gateway/team/teams/create` | `name:string` 必填;`description:string|null`;`ownerUserId:string|null`;`coordinationMode:string`;`objective:string|null`;`policy:TeamPolicy`;`metadata:object` | `Team` | 创建团队,不包含成员数组 |
+| `POST /gateway/team/teams/createWithMembers` | `team:TeamCreateInput` 必填;`members:Array<{agentId:string,role:string,responsibility:string|null,orderIndex:integer,config:object}>` | `team:Team`;`members:TeamMember[]` | 创建团队并写入成员绑定表 |
+| `POST /gateway/team/teams/update` | `teamId:string` 必填;其余字段同 create,均可选 | `Team` | 更新团队 |
+| `POST /gateway/team/teams/delete` | `teamId:string` 必填;`deleteRuns:boolean` 默认 `false` | `deleted:boolean`;`teamId:string` | 删除团队 |
+| `POST /gateway/team/teamMembers/list` | `PageRequest`;`teamId:string` 必填 | `PageResult<TeamMember>` | 团队成员列表 |
+| `POST /gateway/team/teamMembers/add` | `teamId:string` 必填;`agentId:string` 必填;`role:string` 必填;`responsibility:string|null`;`orderIndex:integer|null`;`config:object` | `TeamMember` | 新增团队成员 |
+| `POST /gateway/team/teamMembers/update` | `memberId:string` 必填;`role:string|null`;`responsibility:string|null`;`orderIndex:integer|null`;`config:object|null` | `TeamMember` | 更新团队成员 |
+| `POST /gateway/team/teamMembers/remove` | `memberId:string` 必填 | `deleted:boolean`;`memberId:string` | 删除团队成员 |
+| `POST /gateway/team/teamMembers/sync` | `teamId:string` 必填;`members:Array<{agentId:string,role:string,responsibility:string|null,orderIndex:integer,config:object}>` | `team:Team`;`members:TeamMember[]` | 用当前选择结果同步中间表 |
+| `POST /gateway/team/teamRuns/list` | `PageRequest`;`teamId:string|null`;`sessionId:string|null`;`status:string|null`;`startTime:datetime|null`;`endTime:datetime|null` | `PageResult<TeamRun>` | 团队运行列表 |
+| `POST /gateway/team/teamRuns/start` | `teamId:string` 必填;`sessionId:string|null`;`inputText:string|null`;`input:object|null` | `TeamRun` | 启动团队运行 |
+| `POST /gateway/team/teamRuns/get` | `runId:string` 必填 | `TeamRun` | 团队运行详情 |
+| `POST /gateway/team/teamRuns/cancel` | `runId:string` 必填;`reason:string|null` | `TeamRun` | 取消团队运行 |
+
+`TeamCreateInput`
+
+| 字段 | 类型 | 必填 | 说明 |
+| --- | --- | --- | --- |
+| `name` | string | 是 | 团队名称 |
+| `description` | string \| null | 否 | 说明 |
+| `ownerUserId` | string \| null | 否 | 创建人 ID |
+| `coordinationMode` | string | 是 | 协作模式 |
+| `objective` | string \| null | 否 | 目标 |
+| `policy` | `TeamPolicy` | 是 | 团队策略 |
+| `metadata` | object | 否 | 扩展信息 |
+
+### 5.11 workflowService
+
+`Workflow`
+
+| 字段 | 类型 | 必填 | 说明 |
+| --- | --- | --- | --- |
+| `id` | string | 是 | 工作流 ID |
+| `appId` | string | 是 | 应用 ID |
+| `name` | string | 是 | 名称 |
+| `workflowType` | string | 是 | 工作流类型 |
+| `dsl` | `WorkflowDsl` | 是 | 设计器 DSL |
+| `createdTime` | datetime | 是 | 创建时间 |
+| `updatedTime` | datetime | 是 | 更新时间 |
+
+`WorkflowDsl`
+
+| 字段 | 类型 | 必填 | 说明 |
+| --- | --- | --- | --- |
+| `name` | string | 是 | 工作流名称 |
+| `nodes` | `WorkflowNode[]` | 是 | 节点列表 |
+| `edges` | `WorkflowEdge[]` | 是 | 连线列表 |
+
+| 接口 | 输入参数 | 输出 | 说明 |
+| --- | --- | --- | --- |
+| `POST /gateway/workflow/apps/list` | `PageRequest` | `PageResult<App>` | 应用列表 |
+| `POST /gateway/workflow/apps/create` | `name:string` 必填;`description:string|null`;`ownerUserId:string|null`;`settings:object` | `App` | 创建应用 |
+| `POST /gateway/workflow/apps/update` | `appId:string` 必填;`name:string|null`;`description:string|null`;`settings:object|null` | `App` | 更新应用 |
+| `POST /gateway/workflow/apps/delete` | `appId:string` 必填 | `deleted:boolean`;`appId:string` | 删除应用 |
+| `POST /gateway/workflow/workflows/list` | `PageRequest`;`appId:string|null`;`workflowType:string|null` | `PageResult<Workflow>` | 工作流列表 |
+| `POST /gateway/workflow/workflows/get` | `workflowId:string` 必填 | `Workflow` | 工作流详情 |
+| `POST /gateway/workflow/workflows/create` | `appId:string` 必填;`name:string` 必填;`workflowType:string` 必填;`dsl:WorkflowDsl` | `Workflow` | 创建工作流 |
+| `POST /gateway/workflow/workflows/save` | `workflowId:string` 必填;`name:string|null`;`dsl:WorkflowDsl` 必填 | `Workflow` | 保存工作流 |
+| `POST /gateway/workflow/workflows/delete` | `workflowId:string` 必填 | `deleted:boolean`;`workflowId:string` | 删除工作流 |
+| `POST /gateway/workflow/workflows/validate` | `dsl:WorkflowDsl` 必填 | `valid:boolean`;`diagnostics:Array<{severity:string,diagnosticId:string,message:string,nodeId:string|null,edgeIndex:integer|null}>`;`nodeCount:integer`;`edgeCount:integer`;`entryNodeIds:string[]`;`terminalNodeIds:string[]`;`isolatedNodeIds:string[]`;`unreachableNodeIds:string[]`;`cycleDetected:boolean` | 校验工作流 |
+| `POST /gateway/workflow/debug/start` | `workflowId:string|null`;`dsl:WorkflowDsl|null`;`input:object`;`breakpoints:string[]` | `debugSessionId:string`;`runId:string`;`createdTime:datetime` | 启动调试 |
+| `POST /gateway/workflow/debug/step` | `debugSessionId:string` 必填;`action:"next"|"continue"|"pause"` 必填;`inputPatch:object|null` | `run:WorkflowRun`;`currentNode:NodeRun|null`;`logs:object[]` | 单步调试 |
+| `POST /gateway/workflow/debug/stop` | `debugSessionId:string` 必填 | `stopped:boolean`;`finishedTime:datetime` | 停止调试 |
+
+### 5.12 runtimeService
+
+`WorkflowRun`
+
+| 字段 | 类型 | 必填 | 说明 |
+| --- | --- | --- | --- |
+| `id` | string | 是 | 运行 ID |
+| `appId` | string | 是 | 应用 ID |
+| `workflowId` | string | 是 | 工作流 ID |
+| `sessionId` | string \| null | 否 | 会话 ID |
+| `parentRunId` | string \| null | 否 | 父运行 ID |
+| `rootRunId` | string \| null | 否 | 根运行 ID |
+| `runType` | string | 是 | 运行类型 |
+| `status` | `"pending"` \| `"running"` \| `"completed"` \| `"failed"` \| `"cancelled"` \| `"paused"` | 是 | 状态 |
+| `triggerType` | string | 是 | 触发类型 |
+| `priority` | integer | 是 | 优先级 |
+| `currentNodeCount` | integer | 是 | 当前节点数 |
+| `startedTime` | datetime \| null | 否 | 开始时间 |
+| `finishedTime` | datetime \| null | 否 | 结束时间 |
+| `createdTime` | datetime | 是 | 创建时间 |
+
+`NodeRun`
+
+| 字段 | 类型 | 必填 | 说明 |
+| --- | --- | --- | --- |
+| `id` | string | 是 | 节点运行 ID |
+| `runId` | string | 是 | 运行 ID |
+| `nodeId` | string | 是 | 节点 ID |
+| `nodeType` | string | 是 | 节点类型 |
+| `attemptNo` | integer | 是 | 第几次尝试 |
+| `status` | `"pending"` \| `"queued"` \| `"running"` \| `"completed"` \| `"failed"` \| `"skipped"` | 是 | 状态 |
+| `outputText` | string \| null | 否 | 输出文本 |
+| `output` | object \| null | 否 | 结构化输出 |
+| `scheduledTime` | datetime \| null | 否 | 计划时间 |
+| `timeoutTime` | datetime \| null | 否 | 超时时间 |
+| `queuedTime` | datetime \| null | 否 | 排队时间 |
+| `createdTime` | datetime | 是 | 创建时间 |
+
+| 接口 | 输入参数 | 输出 | 说明 |
+| --- | --- | --- | --- |
+| `POST /gateway/runtime/runs/list` | `PageRequest`;`appId:string|null`;`workflowId:string|null`;`sessionId:string|null`;`status:string|null`;`startTime:datetime|null`;`endTime:datetime|null` | `PageResult<WorkflowRun>` | 运行列表 |
+| `POST /gateway/runtime/runs/get` | `runId:string` 必填 | `WorkflowRun` | 运行详情 |
+| `POST /gateway/runtime/nodeRuns/list` | `PageRequest`;`runId:string` 必填;`status:string|null` | `PageResult<NodeRun>` | 节点运行 |
+| `POST /gateway/runtime/executionLogs/list` | `PageRequest`;`runId:string` 必填;`nodeRunId:string|null`;`level:string|null` | `PageResult<object>` | 执行日志 |
+| `POST /gateway/runtime/traceSpans/list` | `PageRequest`;`runId:string` 必填;`nodeRunId:string|null` | `PageResult<object>` | Trace 数据 |
+
+## 6. 新建智能体勾选技能的标准流程
+
+这个流程解决“界面勾选技能”和“数据库必须中间表”的冲突。
+
+1. 打开弹窗时,前端调用 `POST /gateway/model/models/list` 获取可选模型。
+2. 打开弹窗时,前端调用 `POST /gateway/skill/skills/list` 获取可选技能。
+3. 用户填写智能体名称、提示词、记忆策略、运行策略,并勾选技能。
+4. 前端提交 `POST /gateway/agent/agents/createWithBindings`。
+5. agentService 在一个事务内创建 `Agent`,再批量创建 `AgentSkillBinding`。
+6. 返回 `agent` 和 `skillBindings`,前端直接刷新列表和详情。
+
+请求体:
+
+| 字段 | 类型 | 必填 | 说明 |
+| --- | --- | --- | --- |
+| `agent` | `AgentCreateInput` | 是 | 智能体基础信息 |
+| `skillBindings` | `Array<{skillId:string,orderIndex:integer,config:object}>` | 是 | 勾选技能对应的绑定行 |
+
+响应体:
+
+| 字段 | 类型 | 必填 | 说明 |
+| --- | --- | --- | --- |
+| `agent` | `Agent` | 是 | 创建后的智能体 |
+| `skillBindings` | `AgentSkillBinding[]` | 是 | 创建后的绑定行 |
+
+编辑智能体时,如果用户重新勾选技能,前端调用 `POST /gateway/agent/agentSkillBindings/sync`。该接口以当前勾选结果为准,由 agentService 自动新增、更新、删除绑定行。
+
+## 7. 前端迁移清单
+
+| 优先级 | 任务 | 说明 |
+| --- | --- | --- |
+| P0 | API Client 改造 | 移除 `get`、`patch`、`delete` 调用,统一 `post` |
+| P0 | DTO 改造 | 所有请求和响应字段改为小驼峰 |
+| P0 | 时间字段改造 | 所有时间字段改为 `createdTime`、`updatedTime`、`startedTime` 这类命名 |
+| P0 | Agent 创建闭环 | 创建弹窗改调 `agents/createWithBindings` |
+| P0 | Agent 编辑闭环 | 技能勾选变更改调 `agentSkillBindings/sync` |
+| P0 | Skill 工具绑定 | 技能页改用 `skillToolBindings/sync` |
+| P0 | Team 成员绑定 | 团队页改用 `teamMembers/sync` |
+| P1 | MCP 闭环 | 工具页支持 `importConfig`、`test`、`discoverTools` |
+| P1 | Knowledge 落库 | Jobs、Evaluation、Settings 全部接 knowledgeService |
+| P1 | Skills 去 mock | Skills 页接 skillService |
+| P2 | Workflow 设计器 | 挂路由并接入 workflowService |
+

+ 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]:

+ 6 - 36
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 (
@@ -56,17 +55,6 @@ from .model_contracts import (
     ChatCompletionResponseContract,
     ChatMessageContract,
 )
-from .runtime_contracts import (
-    InitialNodeContract,
-    NodeRunContract,
-    NodeRunStatus,
-    NodeRunStatusUpdateContract,
-    RunBootstrapContract,
-    RunCreateContract,
-    WorkflowRunContract,
-    WorkflowRunStatus,
-    WorkflowRunStatusUpdateContract,
-)
 from .scheduler_contracts import ScheduledJobContract, ScheduledJobStatus, ScheduledJobType
 from .service import ServiceDescriptor, ServiceHealth
 from .skill_contracts import (
@@ -76,8 +64,6 @@ from .skill_contracts import (
     SkillRunContract,
     SkillRunStatus,
     SkillStatus,
-    SkillVersionContract,
-    SkillVersionStatus,
 )
 from .team_contracts import (
     TeamDefinitionContract,
@@ -86,8 +72,7 @@ from .team_contracts import (
     TeamRunContract,
     TeamRunStatus,
     TeamStatus,
-    TeamVersionContract,
-    TeamVersionStatus,
+    TeamConfigContract,
 )
 from .tool_contracts import (
     ToolBindingContract,
@@ -95,9 +80,8 @@ from .tool_contracts import (
     ToolCredentialContract,
     ToolCredentialRevealContract,
     ToolDefinitionContract,
-    ToolVersionContract,
+    ToolConnectionContract,
 )
-from .workflow_contracts import WorkflowVersionContract
 
 __all__ = [
     "AgentDefinitionContract",
@@ -110,8 +94,7 @@ __all__ = [
     "AgentToolInvocationContract",
     "AgentToolInvocationStatus",
     "AgentToolRefContract",
-    "AgentVersionContract",
-    "AgentVersionStatus",
+    "AgentConfigContract",
     "PermissionCheckContract",
     "PermissionCheckResultContract",
     "RoleAssignmentContract",
@@ -129,7 +112,6 @@ __all__ = [
     "HumanTaskCreateContract",
     "HumanTaskStatus",
     "HumanTaskType",
-    "InitialNodeContract",
     "KnowledgeBaseContract",
     "KnowledgeBaseStatus",
     "KnowledgeChunkContract",
@@ -147,11 +129,6 @@ __all__ = [
     "NodeExecutionRequestContract",
     "NodeExecutionResultContract",
     "RunExecutionRequestContract",
-    "NodeRunContract",
-    "NodeRunStatus",
-    "NodeRunStatusUpdateContract",
-    "RunBootstrapContract",
-    "RunCreateContract",
     "ScheduledJobContract",
     "ScheduledJobStatus",
     "ScheduledJobType",
@@ -163,24 +140,17 @@ __all__ = [
     "SkillRunContract",
     "SkillRunStatus",
     "SkillStatus",
-    "SkillVersionContract",
-    "SkillVersionStatus",
     "TeamDefinitionContract",
     "TeamMemberContract",
     "TeamMemberRole",
     "TeamRunContract",
     "TeamRunStatus",
     "TeamStatus",
-    "TeamVersionContract",
-    "TeamVersionStatus",
+    "TeamConfigContract",
     "ToolBindingContract",
     "ToolBindingDetailContract",
     "ToolCredentialContract",
     "ToolCredentialRevealContract",
     "ToolDefinitionContract",
-    "ToolVersionContract",
-    "WorkflowRunStatus",
-    "WorkflowRunStatusUpdateContract",
-    "WorkflowRunContract",
-    "WorkflowVersionContract",
+    "ToolConnectionContract",
 ]

+ 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

+ 3 - 1
libs/core-domain/src/core_domain/execution_contracts.py

@@ -1,7 +1,9 @@
+from typing import Literal
+
 from core_shared import JSONValue
 from pydantic import BaseModel, Field
 
-from .runtime_contracts import NodeRunStatus
+NodeRunStatus = Literal["pending", "queued", "running", "completed", "failed", "skipped"]
 
 
 class NodeExecutionRequestContract(BaseModel):

+ 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):

+ 0 - 81
libs/core-domain/src/core_domain/runtime_contracts.py

@@ -1,81 +0,0 @@
-from datetime import datetime
-from typing import Literal
-
-from core_shared import JSONValue
-from pydantic import BaseModel
-
-NodeRunStatus = Literal["pending", "queued", "running", "completed", "failed", "skipped"]
-WorkflowRunStatus = Literal["pending", "running", "completed", "failed", "cancelled", "paused"]
-
-
-class InitialNodeContract(BaseModel):
-    node_id: str
-    node_type: str
-    status: NodeRunStatus = "queued"
-
-
-class RunCreateContract(BaseModel):
-    app_id: str
-    app_version_id: str
-    workflow_id: str
-    workflow_version_id: str
-    session_id: str | None = None
-    parent_run_id: str | None = None
-    root_run_id: str | None = None
-    run_type: str = "main"
-    trigger_type: str = "user"
-    priority: int = 0
-    initial_node: InitialNodeContract | None = None
-
-
-class WorkflowRunContract(BaseModel):
-    id: str
-    app_id: str
-    app_version_id: str
-    workflow_id: str
-    workflow_version_id: str
-    session_id: str | None = None
-    parent_run_id: str | None = None
-    root_run_id: str | None = None
-    run_type: str
-    status: WorkflowRunStatus
-    trigger_type: str
-    priority: int
-    current_node_count: int
-    started_time: datetime | None = None
-    created_time: datetime
-
-
-class NodeRunContract(BaseModel):
-    id: str
-    run_id: str
-    node_id: str
-    node_type: str
-    attempt_no: int
-    status: NodeRunStatus
-    output_text: str | None = None
-    output_json: dict[str, JSONValue] | None = None
-    scheduled_time: datetime | None = None
-    timeout_time: datetime | None = None
-    queued_time: datetime | None = None
-    created_time: datetime
-
-
-class RunBootstrapContract(BaseModel):
-    run: WorkflowRunContract
-    initial_node: NodeRunContract | None = None
-
-
-class WorkflowRunStatusUpdateContract(BaseModel):
-    status: WorkflowRunStatus
-    error_code: str | None = None
-    error_message: str | None = None
-
-
-class NodeRunStatusUpdateContract(BaseModel):
-    status: NodeRunStatus
-    worker_key: str | None = None
-    error_code: str | None = None
-    error_message: str | None = None
-    output_text: str | None = None
-    output_json: dict[str, JSONValue] | 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

+ 0 - 17
libs/core-domain/src/core_domain/workflow_contracts.py

@@ -1,17 +0,0 @@
-from datetime import datetime
-
-from core_shared import JSONValue
-from pydantic import BaseModel
-
-
-class WorkflowVersionContract(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
-

+ 0 - 19
libs/core-dsl/pyproject.toml

@@ -1,19 +0,0 @@
-[build-system]
-requires = ["setuptools>=68"]
-build-backend = "setuptools.build_meta"
-
-[project]
-name = "core-dsl"
-version = "0.1.0"
-description = "Workflow DSL models for agent platform."
-requires-python = ">=3.11"
-dependencies = [
-  "core-shared",
-  "pydantic>=2.7,<3.0",
-]
-
-[tool.setuptools]
-package-dir = {"" = "src"}
-
-[tool.setuptools.packages.find]
-where = ["src"]

+ 0 - 19
libs/core-dsl/src/core_dsl/__init__.py

@@ -1,19 +0,0 @@
-from .workflow import (
-    EdgeDefinition,
-    NodeDefinition,
-    WorkflowDefinition,
-    get_initial_node_definition,
-    get_node_definition,
-    get_successor_node_definitions,
-    parse_workflow_definition,
-)
-
-__all__ = [
-    "EdgeDefinition",
-    "NodeDefinition",
-    "WorkflowDefinition",
-    "get_initial_node_definition",
-    "get_node_definition",
-    "get_successor_node_definitions",
-    "parse_workflow_definition",
-]

+ 0 - 53
libs/core-dsl/src/core_dsl/workflow.py

@@ -1,53 +0,0 @@
-from core_shared import JSONValue
-from pydantic import BaseModel, Field
-
-
-class NodeDefinition(BaseModel):
-    id: str
-    type: str
-    name: str | None = None
-    config: dict[str, JSONValue] = Field(default_factory=dict)
-
-
-class EdgeDefinition(BaseModel):
-    source: str
-    target: str
-    condition: str | None = None
-
-
-class WorkflowDefinition(BaseModel):
-    code: str
-    name: str = "workflow"
-    nodes: list[NodeDefinition] = Field(default_factory=list)
-    edges: list[EdgeDefinition] = Field(default_factory=list)
-
-
-def parse_workflow_definition(payload: dict[str, JSONValue] | None) -> WorkflowDefinition | None:
-    if payload is None:
-        return None
-    return WorkflowDefinition.model_validate(payload)
-
-
-def get_node_definition(workflow: WorkflowDefinition, node_id: str) -> NodeDefinition | None:
-    for node in workflow.nodes:
-        if node.id == node_id:
-            return node
-    return None
-
-
-def get_initial_node_definition(workflow: WorkflowDefinition) -> NodeDefinition | None:
-    incoming_targets = {edge.target for edge in workflow.edges}
-    for node in workflow.nodes:
-        if node.id not in incoming_targets:
-            return node
-    if workflow.nodes:
-        return workflow.nodes[0]
-    return None
-
-
-def get_successor_node_definitions(
-    workflow: WorkflowDefinition,
-    current_node_id: str) -> list[NodeDefinition]:
-    successor_ids = [edge.target for edge in workflow.edges if edge.source == current_node_id]
-    node_map = {node.id: node for node in workflow.nodes}
-    return [node_map[item] for item in successor_ids if item in node_map]

+ 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

+ 0 - 3
pyproject.toml

@@ -2,7 +2,6 @@
 members = [
   "libs/core-db",
   "libs/core-domain",
-  "libs/core-dsl",
   "libs/core-events",
   "libs/core-shared",
   "services/api-gateway",
@@ -18,8 +17,6 @@ members = [
   "services/session-service",
   "services/skill-service",
   "services/team-service",
-  "services/workflow-service",
-  "services/runtime-service",
   "services/tool-service",
 ]
 

+ 62 - 5
scripts/migrate_all.py

@@ -2,16 +2,15 @@ from __future__ import annotations
 
 import argparse
 import os
+import shutil
 import subprocess
 import sys
 from dataclasses import dataclass
 from pathlib import Path
 
 DEFAULT_SERVICE_ORDER = [
-    "workflow-service",
     "session-service",
     "tool-service",
-    "runtime-service",
     "model-gateway-service",
     "memory-service",
     "skill-service",
@@ -45,10 +44,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 +77,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 +124,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 +139,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())

+ 0 - 370
scripts/smoke_runtime_no_key.py

@@ -1,370 +0,0 @@
-from __future__ import annotations
-
-import json
-import os
-import sys
-import uuid
-from dataclasses import dataclass
-
-import httpx
-
-WORKFLOW_SERVICE_URL = os.getenv(
-    "AGENT_PLATFORM_SMOKE_WORKFLOW_URL",
-    "http://127.0.0.1:8002/workflows")
-RUNTIME_SERVICE_URL = os.getenv(
-    "AGENT_PLATFORM_SMOKE_RUNTIME_URL",
-    "http://127.0.0.1:8003/runtime")
-SMOKE_API_KEY = os.getenv("AGENT_PLATFORM_SMOKE_API_KEY")
-
-
-@dataclass(frozen=True)
-class SmokeScenario:
-    score: int
-    expected_branch_node_id: str
-    expected_output_text: str
-
-
-SCENARIOS = (
-    SmokeScenario(
-        score=7,
-        expected_branch_node_id="high_path",
-        expected_output_text="Alice passed with score 7"),
-    SmokeScenario(
-        score=3,
-        expected_branch_node_id="low_path",
-        expected_output_text="Alice did not pass; score 3"))
-
-
-def main() -> int:
-    unique_suffix = uuid.uuid4().hex[:8]
-    headers = {}
-    if SMOKE_API_KEY:
-        headers["x-api-key"] = SMOKE_API_KEY
-
-    with httpx.Client(timeout=20.0, headers=headers) as client:
-        app_id = create_app(client, unique_suffix)
-        workflow_id = create_workflow(client, app_id, unique_suffix)
-
-        results: list[dict[str, object]] = []
-        for scenario in SCENARIOS:
-            results.append(run_scenario(client, app_id, workflow_id, unique_suffix, scenario))
-        results.append(run_retriever_scenario(client, app_id, workflow_id, unique_suffix))
-
-    print(json.dumps(results, ensure_ascii=False, indent=2))
-    return 0
-
-
-def create_app(client: httpx.Client, unique_suffix: str) -> str:
-    response = client.post(
-        f"{WORKFLOW_SERVICE_URL}/apps",
-        json={
-            "code": f"smoke-app-{unique_suffix}",
-            "name": f"Smoke App {unique_suffix}",
-        })
-    response.raise_for_status()
-    payload = response.json()
-    return str(payload["id"])
-
-
-def create_workflow(client: httpx.Client, app_id: str, unique_suffix: str) -> str:
-    response = client.post(
-        WORKFLOW_SERVICE_URL,
-        json={
-            "app_id": app_id,
-            "code": f"smoke-flow-{unique_suffix}",
-            "name": f"Smoke Flow {unique_suffix}",
-        })
-    response.raise_for_status()
-    payload = response.json()
-    return str(payload["id"])
-
-
-def run_scenario(
-    client: httpx.Client,
-    app_id: str,
-    workflow_id: str,
-    unique_suffix: str,
-    scenario: SmokeScenario) -> dict[str, object]:
-    workflow_version_id = create_workflow_version(client, workflow_id, unique_suffix, scenario.score)
-    app_version_id = create_app_version(client, app_id, workflow_version_id)
-    run_id = create_run(client, app_id, app_version_id, workflow_id, workflow_version_id)
-    execute_run(client, run_id)
-    node_runs = list_node_runs(client, run_id)
-    artifacts = list_node_artifacts(client, run_id)
-    if len(artifacts) < 3:
-        raise AssertionError(f"expected at least 3 artifacts, got {len(artifacts)}")
-    trace_spans = list_trace_spans(client, run_id)
-    if len(trace_spans) < 3:
-        raise AssertionError(f"expected at least 3 trace spans, got {len(trace_spans)}")
-
-    node_map = {str(item["node_id"]): item for item in node_runs}
-    assert scenario.expected_branch_node_id in node_map, (
-        f"expected branch node not found: {scenario.expected_branch_node_id}"
-    )
-    expected_node = node_map[scenario.expected_branch_node_id]
-    actual_output_text = expected_node.get("output_text")
-    if actual_output_text != scenario.expected_output_text:
-        raise AssertionError(
-            f"unexpected output_text for {scenario.expected_branch_node_id}: {actual_output_text!r}"
-        )
-
-    other_branch_node_id = "low_path" if scenario.expected_branch_node_id == "high_path" else "high_path"
-    if other_branch_node_id in node_map:
-        raise AssertionError(f"unexpected branch node executed: {other_branch_node_id}")
-
-    return {
-        "score": scenario.score,
-        "executed_node_ids": [str(item["node_id"]) for item in node_runs],
-        "branch_output_text": actual_output_text,
-        "artifact_count": len(artifacts),
-        "trace_span_count": len(trace_spans),
-    }
-
-
-def run_retriever_scenario(
-    client: httpx.Client,
-    app_id: str,
-    workflow_id: str,
-    unique_suffix: str) -> dict[str, object]:
-    workflow_version_id = create_retriever_workflow_version(client, workflow_id, unique_suffix)
-    app_version_id = create_app_version(client, app_id, workflow_version_id)
-    run_id = create_run(client, app_id, app_version_id, workflow_id, workflow_version_id)
-    execute_run(client, run_id)
-    node_runs = list_node_runs(client, run_id)
-    artifacts = list_node_artifacts(client, run_id)
-    if len(artifacts) < 3:
-        raise AssertionError(f"expected at least 3 retriever artifacts, got {len(artifacts)}")
-    trace_spans = list_trace_spans(client, run_id)
-    if len(trace_spans) < 3:
-        raise AssertionError(f"expected at least 3 retriever trace spans, got {len(trace_spans)}")
-
-    node_map = {str(item["node_id"]): item for item in node_runs}
-    answer_node = node_map.get("render_answer")
-    if answer_node is None:
-        raise AssertionError("retriever answer node was not executed")
-    answer_text = answer_node.get("output_text")
-    expected_answer_text = "Top doc: Refund Policy"
-    if answer_text != expected_answer_text:
-        raise AssertionError(f"unexpected retriever answer text: {answer_text!r}")
-
-    retrieve_node = node_map.get("retrieve_docs")
-    if retrieve_node is None:
-        raise AssertionError("retriever node was not executed")
-    retrieve_output = retrieve_node.get("output_json")
-    if not isinstance(retrieve_output, dict):
-        raise AssertionError("retriever output_json must be an object")
-
-    return {
-        "scenario": "retriever",
-        "executed_node_ids": [str(item["node_id"]) for item in node_runs],
-        "answer_text": answer_text,
-        "artifact_count": len(artifacts),
-        "trace_span_count": len(trace_spans),
-    }
-
-
-def create_workflow_version(
-    client: httpx.Client,
-    workflow_id: str,
-    unique_suffix: str,
-    score: int) -> str:
-    response = client.post(
-        f"{WORKFLOW_SERVICE_URL}/versions",
-        json={
-            "workflow_id": workflow_id,
-            "status": "active",
-            "dsl_json": build_workflow_dsl(unique_suffix, score),
-        })
-    response.raise_for_status()
-    payload = response.json()
-    return str(payload["id"])
-
-
-def create_retriever_workflow_version(
-    client: httpx.Client,
-    workflow_id: str,
-    unique_suffix: str) -> str:
-    response = client.post(
-        f"{WORKFLOW_SERVICE_URL}/versions",
-        json={
-            "workflow_id": workflow_id,
-            "status": "active",
-            "dsl_json": build_retriever_workflow_dsl(unique_suffix),
-        })
-    response.raise_for_status()
-    payload = response.json()
-    return str(payload["id"])
-
-
-def create_app_version(client: httpx.Client, app_id: str, workflow_version_id: str) -> str:
-    response = client.post(
-        f"{WORKFLOW_SERVICE_URL}/apps/versions",
-        json={
-            "app_id": app_id,
-            "workflow_version_id": workflow_version_id,
-            "status": "active",
-        })
-    response.raise_for_status()
-    payload = response.json()
-    return str(payload["id"])
-
-
-def create_run(
-    client: httpx.Client,
-    app_id: str,
-    app_version_id: str,
-    workflow_id: str,
-    workflow_version_id: str) -> str:
-    response = client.post(
-        f"{RUNTIME_SERVICE_URL}/runs",
-        json={
-            "app_id": app_id,
-            "app_version_id": app_version_id,
-            "workflow_id": workflow_id,
-            "workflow_version_id": workflow_version_id,
-        })
-    response.raise_for_status()
-    payload = response.json()
-    return str(payload["run"]["id"])
-
-
-def execute_run(client: httpx.Client, run_id: str) -> None:
-    response = client.post(
-        f"{RUNTIME_SERVICE_URL}/runs/{run_id}/execute",
-        json={"max_steps": 8})
-    response.raise_for_status()
-
-
-def list_node_runs(client: httpx.Client, run_id: str) -> list[dict[str, object]]:
-    response = client.get(
-        f"{RUNTIME_SERVICE_URL}/node-runs",
-        params={"run_id": run_id})
-    response.raise_for_status()
-    payload = response.json()
-    if not isinstance(payload, list):
-        raise AssertionError("node-runs response must be a list")
-    return [item for item in payload if isinstance(item, dict)]
-
-
-def list_node_artifacts(client: httpx.Client, run_id: str) -> list[dict[str, object]]:
-    response = client.get(
-        f"{RUNTIME_SERVICE_URL}/node-artifacts",
-        params={"run_id": run_id})
-    response.raise_for_status()
-    payload = response.json()
-    if not isinstance(payload, list):
-        raise AssertionError("node-artifacts response must be a list")
-    return [item for item in payload if isinstance(item, dict)]
-
-
-def list_trace_spans(client: httpx.Client, run_id: str) -> list[dict[str, object]]:
-    response = client.get(
-        f"{RUNTIME_SERVICE_URL}/trace-spans",
-        params={"run_id": run_id})
-    response.raise_for_status()
-    payload = response.json()
-    if not isinstance(payload, list):
-        raise AssertionError("trace-spans response must be a list")
-    return [item for item in payload if isinstance(item, dict)]
-
-
-def build_workflow_dsl(unique_suffix: str, score: int) -> dict[str, object]:
-    return {
-        "code": f"smoke-flow-{unique_suffix}-{score}",
-        "name": f"Smoke Flow {score}",
-        "nodes": [
-            {
-                "id": "seed_state",
-                "type": "assigner",
-                "config": {
-                    "assignments": {
-                        "score": score,
-                        "user_name": "Alice",
-                    },
-                },
-            },
-            {
-                "id": "check_score",
-                "type": "if-else",
-                "config": {
-                    "expression": "state.score >= 5",
-                },
-            },
-            {
-                "id": "high_path",
-                "type": "template-transform",
-                "config": {
-                    "template": "{{state.user_name}} passed with score {{state.score}}",
-                },
-            },
-            {
-                "id": "low_path",
-                "type": "template-transform",
-                "config": {
-                    "template": "{{state.user_name}} did not pass; score {{state.score}}",
-                },
-            },
-        ],
-        "edges": [
-            {"source": "seed_state", "target": "check_score"},
-            {"source": "check_score", "target": "high_path", "condition": "true"},
-            {"source": "check_score", "target": "low_path", "condition": "false"},
-        ],
-    }
-
-
-def build_retriever_workflow_dsl(unique_suffix: str) -> dict[str, object]:
-    return {
-        "code": f"smoke-retriever-{unique_suffix}",
-        "name": "Smoke Retriever Flow",
-        "nodes": [
-            {
-                "id": "seed_query",
-                "type": "assigner",
-                "config": {
-                    "assignments": {
-                        "query": "refund policy",
-                    },
-                },
-            },
-            {
-                "id": "retrieve_docs",
-                "type": "knowledge-retrieval",
-                "config": {
-                    "query_template": "{{state.query}}",
-                    "top_k": 1,
-                    "documents": [
-                        {
-                            "id": "shipping",
-                            "title": "Shipping Policy",
-                            "text": "Shipping usually takes three to five business days.",
-                        },
-                        {
-                            "id": "refund",
-                            "title": "Refund Policy",
-                            "text": "Refund policy allows returns within seven days after delivery.",
-                        },
-                    ],
-                },
-            },
-            {
-                "id": "render_answer",
-                "type": "template-transform",
-                "config": {
-                    "template": "Top doc: {{nodes.retrieve_docs.output.retrieved_documents.0.title}}",
-                },
-            },
-        ],
-        "edges": [
-            {"source": "seed_query", "target": "retrieve_docs"},
-            {"source": "retrieve_docs", "target": "render_answer"},
-        ],
-    }
-
-
-if __name__ == "__main__":
-    try:
-        raise SystemExit(main())
-    except Exception as exc:
-        print(f"smoke test failed: {exc}", file=sys.stderr)
-        raise

+ 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

+ 183 - 14
services/agent-service/app/api/routes.py

@@ -1,5 +1,8 @@
+import json
+
 from core_domain import ServiceHealth
 from fastapi import APIRouter, Depends, HTTPException, Query
+from fastapi.responses import StreamingResponse
 from sqlalchemy import text
 from sqlalchemy.orm import Session
 
@@ -7,24 +10,40 @@ from app.application.services import AgentApplicationService, build_agent_applic
 from app.bootstrap.settings import AgentServiceSettings
 from app.db.session import get_db
 from app.schemas.agent import (
+    AgentConfigCreateRequest,
+    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()
 
 
+def json_dump(payload: dict[str, object]) -> str:
+    return json.dumps(payload, ensure_ascii=False, default=str)
+
+
 def get_agent_service_settings() -> AgentServiceSettings:
     return AgentServiceSettings()
 
@@ -55,6 +74,33 @@ 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,
+    service: AgentApplicationService = Depends(get_agent_application_service)) -> AgentResponse:
+    entity = service.update_agent(payload)
+    if entity is None:
+        raise HTTPException(status_code=404, detail=f"agent not found: {payload.agent_id}")
+    return AgentResponse.from_entity(entity)
+
+
 @router.patch("/{agent_id}/status", response_model=AgentResponse)
 def update_agent_status(
     agent_id: str,
@@ -66,24 +112,45 @@ def update_agent_status(
     return AgentResponse.from_entity(entity)
 
 
-@router.post("/versions", response_model=AgentVersionResponse)
-def create_agent_version(
-    payload: AgentVersionCreateRequest,
-    service: AgentApplicationService = Depends(get_agent_application_service)) -> AgentVersionResponse:
+@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,
+    service: AgentApplicationService = Depends(get_agent_application_service)) -> AgentConfigResponse:
     try:
-        entity = service.create_agent_version(payload)
+        entity = service.create_agent_config(payload)
     except ValueError as exc:
         raise HTTPException(status_code=422, detail=str(exc)) from exc
-    return AgentVersionResponse.from_entity(entity)
+    return AgentConfigResponse.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]:
+@router.post("/configs/list", response_model=list[AgentConfigResponse])
+def list_agent_configs(
+    payload: AgentConfigListRequest,
+    service: AgentApplicationService = Depends(get_agent_application_service)) -> list[AgentConfigResponse]:
     return [
-        AgentVersionResponse.from_entity(item)
-        for item in service.list_agent_versions(agent_id=agent_id)
+        AgentConfigResponse.from_entity(item)
+        for item in service.list_agent_configs(agent_id=payload.agent_id)
     ]
 
 
@@ -111,6 +178,28 @@ 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,
+    service: AgentApplicationService = Depends(get_agent_application_service)) -> AgentRunResponse:
+    entity = service.get_agent_run(payload)
+    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.get(
     "/runs/{agent_run_id}/tool-invocations",
     response_model=list[AgentToolInvocationResponse])
@@ -124,6 +213,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,
@@ -135,6 +237,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,
@@ -153,6 +273,55 @@ def execute_agent_run(
         dry_run=dry_run_value if isinstance(dry_run_value, bool) else False)
 
 
+@router.post("/runs/{agent_run_id}/execute-stream")
+def execute_agent_run_stream(
+    agent_run_id: str,
+    payload: AgentRunExecuteRequest,
+    service: AgentApplicationService = Depends(get_agent_application_service)) -> StreamingResponse:
+    if service.get_agent_run(AgentRunDetailRequest(agent_run_id=agent_run_id)) is None:
+        raise HTTPException(status_code=404, detail=f"agent_run not found: {agent_run_id}")
+
+    def events():
+        for item in service.execute_agent_run_stream(agent_run_id=agent_run_id, payload=payload):
+            event = item.get("event")
+            event_name = event if isinstance(event, str) else "message"
+            data = {key: value for key, value in item.items() if key != "event"}
+            yield f"event: {event_name}\ndata: {json_dump(data)}\n\n"
+
+    return StreamingResponse(
+        events(),
+        media_type="text/event-stream",
+        headers=_sse_headers())
+
+
+def _sse_headers() -> dict[str, str]:
+    return {
+        "Cache-Control": "no-cache",
+        "X-Accel-Buffering": "no",
+    }
+
+
+@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,

+ 277 - 95
services/agent-service/app/application/services.py

@@ -1,10 +1,13 @@
 import json
+from collections.abc import Iterator
 from datetime import datetime, timedelta
 from typing import cast
 
 from sqlalchemy.orm import Session
 
 from core_events import EventPublishContract, EventServiceClient, EventServiceClientError
+from uuid import uuid4
+
 from core_domain import (
     AgentSkillRefContract,
     AgentToolRefContract,
@@ -19,23 +22,30 @@ 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
 from app.infrastructure.tool_client import ToolServiceClient, ToolServiceClientError
 from app.schemas.agent import (
     AgentCreateRequest,
+    AgentConfigCreateRequest,
+    AgentConfigListRequest,
     AgentRunCreateRequest,
+    AgentRunDetailRequest,
     AgentRunExecuteRequest,
     AgentRunStatusUpdateRequest,
     AgentStatusUpdateRequest,
-    AgentVersionCreateRequest)
+    AgentUpdateRequest)
+
+
+def generate_agent_code() -> str:
+    return f"agent_{uuid4().hex[:16]}"
 
 
 class AgentApplicationService:
@@ -43,7 +53,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,
@@ -56,7 +66,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
@@ -71,7 +81,7 @@ class AgentApplicationService:
 
     def create_agent(self, payload: AgentCreateRequest) -> AgentDefinition:
         return self.agent_repository.create(
-            code=payload.code,
+            code=payload.code or generate_agent_code(),
             name=payload.name,
             description=payload.description,
             agent_type=payload.agent_type,
@@ -81,6 +91,27 @@ 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,
+            name=payload.name,
+            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,
         *,
@@ -90,15 +121,14 @@ class AgentApplicationService:
             agent_id=agent_id,
             status=payload.status)
 
-    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,
@@ -107,19 +137,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_version_id)
-        if agent_version is None:
-            raise ValueError("published agent version not found")
+            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)
@@ -141,6 +171,10 @@ class AgentApplicationService:
             agent_id=agent_id,
             session_id=session_id)
 
+    def get_agent_run(self, payload: AgentRunDetailRequest) -> AgentRun | None:
+        return self.agent_run_repository.get_by_id(
+            agent_run_id=payload.agent_run_id)
+
     def list_agent_tool_invocations(
         self,
         *,
@@ -176,15 +210,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,
@@ -193,13 +227,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,
@@ -210,10 +244,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": [
@@ -235,10 +269,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,
@@ -247,7 +281,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,
@@ -255,7 +289,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,
@@ -277,17 +311,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,
                     })
             )
@@ -301,7 +335,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,
@@ -310,7 +344,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,
@@ -331,6 +365,135 @@ class AgentApplicationService:
                 })
         return completed_run
 
+    def execute_agent_run_stream(
+        self,
+        *,
+        agent_run_id: str,
+        payload: AgentRunExecuteRequest) -> Iterator[dict[str, JSONValue]]:
+        agent_run = self.agent_run_repository.get_by_id(
+            agent_run_id=agent_run_id)
+        if agent_run is None:
+            return
+
+        agent_config = self.agent_config_repository.get_by_id(
+            agent_config_id=agent_run.agent_config_id)
+        if agent_config is None:
+            failed_run = self.agent_run_repository.update_status(
+                agent_run_id=agent_run.id,
+                status="failed",
+                worker_key=payload.worker_key,
+                error_code="agent_config_missing",
+                error_message=f"agent config not found: {agent_run.agent_config_id}")
+            yield {"event": "agent.run.failed", "run": self._agent_run_to_json(failed_run)}
+            return
+
+        running_run = self.agent_run_repository.update_status(
+            agent_run_id=agent_run.id,
+            status="running",
+            worker_key=payload.worker_key)
+        yield {"event": "agent.run.started", "run": self._agent_run_to_json(running_run)}
+
+        if payload.dry_run or self._read_bool(agent_config.model_config_json, "react_enabled", default=False):
+            completed_run = self.execute_agent_run(agent_run_id=agent_run.id, payload=payload)
+            yield {"event": "agent.run.completed", "run": self._agent_run_to_json(completed_run)}
+            return
+
+        memory_results, memory_metadata = self._read_relevant_memories(
+            agent_run=agent_run,
+            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)
+        tool_invocations = self._invoke_selected_tools(
+            agent_run=agent_run,
+            agent_config=agent_config,
+            selected_tools=selected_tools)
+        skill_invocations = self._invoke_selected_skills(
+            agent_run=agent_run,
+            selected_skills=selected_skills,
+            worker_key=payload.worker_key)
+        messages = self._build_chat_messages(
+            agent_run=agent_run,
+            agent_config=agent_config,
+            memory_results=memory_results,
+            capability_context=self._format_capability_results(
+                tool_invocations=tool_invocations,
+                skill_invocations=skill_invocations))
+
+        if self.model_gateway_client is None:
+            failed_run = self.agent_run_repository.update_status(
+                agent_run_id=agent_run.id,
+                status="failed",
+                worker_key=payload.worker_key,
+                error_code="model_gateway_missing",
+                error_message="model gateway client is not configured",
+                output_json={
+                    "tool_invocations": tool_invocations,
+                    "skill_invocations": skill_invocations,
+                    **memory_metadata,
+                })
+            yield {"event": "agent.run.failed", "run": self._agent_run_to_json(failed_run)}
+            return
+
+        output_parts: list[str] = []
+        try:
+            for delta in self.model_gateway_client.stream_chat_completion(
+                ChatCompletionRequestContract(
+                    model=self._read_optional_string(agent_config.model_config_json, "model"),
+                    temperature=self._read_optional_float(
+                        agent_config.model_config_json,
+                        "temperature"),
+                    max_tokens=self._read_optional_int(
+                        agent_config.model_config_json,
+                        "max_tokens"),
+                    messages=messages,
+                    metadata_json={
+                        "agent_id": agent_run.agent_id,
+                        "agent_config_id": agent_config.id,
+                        "agent_run_id": agent_run.id,
+                    })):
+                output_parts.append(delta)
+                yield {"event": "agent.run.delta", "agent_run_id": agent_run.id, "delta": delta}
+        except ModelGatewayClientError as exc:
+            failed_run = self.agent_run_repository.update_status(
+                agent_run_id=agent_run.id,
+                status="failed",
+                worker_key=payload.worker_key,
+                error_code="model_gateway_error",
+                error_message=str(exc))
+            yield {"event": "agent.run.failed", "run": self._agent_run_to_json(failed_run)}
+            return
+
+        output_text = "".join(output_parts)
+        memory_write_metadata = self._write_interaction_memory(
+            agent_run=agent_run,
+            agent_config=agent_config,
+            output_text=output_text)
+        completed_run = self.agent_run_repository.update_status(
+            agent_run_id=agent_run.id,
+            status="completed",
+            worker_key=payload.worker_key,
+            output_text=output_text,
+            output_json={
+                "dry_run": False,
+                "agent_config_id": agent_config.id,
+                "streamed": True,
+                "tool_invocations": tool_invocations,
+                "skill_invocations": skill_invocations,
+                **memory_metadata,
+                **memory_write_metadata,
+            })
+        if completed_run is not None:
+            self._publish_event(
+                event_type="agent.run.completed",
+                agent_run=completed_run,
+                payload_json={
+                    "agent_run_id": completed_run.id,
+                    "dry_run": False,
+                    "status": completed_run.status,
+                    "streamed": True,
+                })
+        yield {"event": "agent.run.completed", "run": self._agent_run_to_json(completed_run)}
+
     def _publish_event(
         self,
         *,
@@ -350,17 +513,40 @@ 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:
             return
 
+    def _agent_run_to_json(self, agent_run: AgentRun | None) -> dict[str, JSONValue]:
+        if agent_run is None:
+            return {}
+        return {
+            "id": agent_run.id,
+            "agent_id": agent_run.agent_id,
+            "agent_config_id": agent_run.agent_config_id,
+            "session_id": agent_run.session_id,
+            "input_text": agent_run.input_text,
+            "input_json": agent_run.input_json,
+            "output_text": agent_run.output_text,
+            "output_json": agent_run.output_json,
+            "status": agent_run.status,
+            "worker_key": agent_run.worker_key,
+            "queued_time": agent_run.queued_time,
+            "lease_expire_time": agent_run.lease_expire_time,
+            "started_time": agent_run.started_time,
+            "finished_time": agent_run.finished_time,
+            "error_code": agent_run.error_code,
+            "error_message": agent_run.error_message,
+            "created_time": agent_run.created_time,
+        }
+
     def _execute_react_agent_run(
         self,
         *,
         agent_run: AgentRun,
-        agent_version: AgentVersion,
+        agent_config: AgentConfig,
         payload: AgentRunExecuteRequest,
         memory_results: list[MemorySearchResultContract],
         memory_metadata: dict[str, JSONValue],
@@ -380,7 +566,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,
@@ -392,7 +578,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)):
@@ -400,7 +586,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)
                 )
@@ -435,7 +621,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):
@@ -462,7 +648,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
@@ -477,7 +663,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,
@@ -486,7 +672,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,
@@ -557,30 +743,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_published(
+        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(
@@ -604,10 +790,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
@@ -621,10 +807,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):
@@ -635,14 +821,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",
@@ -778,9 +964,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"),
@@ -878,9 +1061,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:
@@ -892,17 +1075,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
@@ -974,23 +1157,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=(
@@ -1003,7 +1186,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,
             })
 
@@ -1075,11 +1258,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:
@@ -1104,17 +1287,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,
@@ -1132,7 +1315,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))
             )
@@ -1154,14 +1337,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"}
 
@@ -1177,20 +1360,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))
@@ -1212,9 +1394,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"
@@ -1285,7 +1467,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)

+ 75 - 34
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,34 @@ 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,
+        *,
+        agent_id: str,
+        name: str | None,
+        description: str | None,
+        metadata_json: dict[str, JSONValue] | None) -> AgentDefinition | None:
+        entity = self.get_by_id(agent_id=agent_id)
+        if entity is None:
+            return None
+        if name is not None:
+            entity.name = name
+        if description is not None:
+            entity.description = description
+        if metadata_json is not None:
+            entity.metadata_json = metadata_json
+        self.db.commit()
+        self.db.refresh(entity)
+        return entity
+
     def update_status(
         self,
         *,
@@ -66,7 +93,7 @@ class AgentDefinitionRepository:
         return entity
 
 
-class AgentVersionRepository:
+class AgentConfigRepository:
     def __init__(self, db: Session) -> None:
         self.db = db
 
@@ -74,62 +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:
+    def get_by_id(self, *, agent_config_id: str) -> AgentConfig | None:
         stmt = (
-            select(AgentVersion)
-            .where(AgentVersion.id == agent_version_id)
+            select(AgentConfig)
+            .where(AgentConfig.id == agent_config_id)
         )
         return self.db.scalar(stmt)
 
-    def get_latest_published(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)
-            .where(AgentVersion.status == "published")
-            .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:
@@ -139,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,
@@ -177,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,
         *,
@@ -266,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,
@@ -275,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,
@@ -297,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"],
+    }

+ 51 - 0
services/agent-service/app/infrastructure/model_gateway_client.py

@@ -1,3 +1,6 @@
+import json
+from collections.abc import Iterator
+
 import httpx
 from core_domain import ChatCompletionRequestContract, ChatCompletionResponseContract
 
@@ -23,3 +26,51 @@ class ModelGatewayClient:
                 return ChatCompletionResponseContract.model_validate(response.json())
         except httpx.HTTPError as exc:
             raise ModelGatewayClientError(f"model-gateway-service request failed: {exc}") from exc
+
+    def stream_chat_completion(
+        self,
+        payload: ChatCompletionRequestContract) -> Iterator[str]:
+        try:
+            with httpx.Client(timeout=self.timeout_seconds) as client:
+                with client.stream(
+                    "POST",
+                    f"{self.base_url}/models/chat-completions/stream",
+                    json=payload.model_dump(mode="json")) as response:
+                    response.raise_for_status()
+                    for event_name, data in _iter_sse_events(response):
+                        if event_name == "delta":
+                            delta = data.get("delta")
+                            if isinstance(delta, str):
+                                yield delta
+                        elif event_name == "error":
+                            message = data.get("message")
+                            raise ModelGatewayClientError(
+                                str(message) if isinstance(message, str) else "model-gateway stream failed")
+        except httpx.HTTPError as exc:
+            raise ModelGatewayClientError(f"model-gateway-service stream failed: {exc}") from exc
+
+
+def _iter_sse_events(response: httpx.Response) -> Iterator[tuple[str, dict[str, object]]]:
+    event_name = "message"
+    data_lines: list[str] = []
+    for line in response.iter_lines():
+        if line == "":
+            if data_lines:
+                yield event_name, _parse_json("\n".join(data_lines))
+            event_name = "message"
+            data_lines = []
+            continue
+        if line.startswith("event:"):
+            event_name = line.removeprefix("event:").strip()
+        elif line.startswith("data:"):
+            data_lines.append(line.removeprefix("data:").strip())
+    if data_lines:
+        yield event_name, _parse_json("\n".join(data_lines))
+
+
+def _parse_json(value: str) -> dict[str, object]:
+    try:
+        payload = json.loads(value)
+    except json.JSONDecodeError:
+        return {}
+    return payload if isinstance(payload, dict) else {}

+ 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,

+ 82 - 10
services/agent-service/app/schemas/agent.py

@@ -11,18 +11,16 @@ 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):
-    code: str
+    code: str | None = None
     name: str
     description: str | None = None
     agent_type: str = "assistant"
@@ -30,20 +28,52 @@ class AgentCreateRequest(BaseModel):
     metadata_json: dict[str, JSONValue] = Field(default_factory=dict)
 
 
+class AgentUpdateRequest(BaseModel):
+    agent_id: str
+    name: str | None = None
+    description: str | None = None
+    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":
         return cls.model_validate(entity, from_attributes=True)
 
 
-class AgentVersionCreateRequest(BaseModel):
+class AgentConfigListRequest(BaseModel):
+    agent_id: str
+
+
+class AgentConfigCreateRequest(BaseModel):
     model_config = ConfigDict(populate_by_name=True)
     agent_id: str
-    status: AgentVersionStatus = "draft"
     role: str = "assistant"
     goal: str | None = None
     system_prompt: str
@@ -55,20 +85,54 @@ class AgentVersionCreateRequest(BaseModel):
     skill_refs: list[AgentSkillRefContract] = Field(default_factory=list)
 
 
-class AgentVersionResponse(AgentVersionContract):
+class AgentConfigResponse(BaseModel):
+    id: str
+    agent_id: str
+    role: str
+    goal: str | None = 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]]
+    created_time: datetime
+
     @classmethod
-    def from_entity(cls, entity: "AgentVersion") -> "AgentVersionResponse":
-        return cls.model_validate(entity, from_attributes=True)
+    def from_entity(cls, entity: "AgentConfig") -> "AgentConfigResponse":
+        return cls(
+            id=entity.id,
+            agent_id=entity.agent_id,
+            role=entity.role,
+            goal=entity.goal,
+            system_prompt=entity.system_prompt,
+            model_config_json=entity.model_config_json,
+            memory_policy_json=entity.memory_policy_json,
+            tool_refs_json=entity.tool_refs_json,
+            skill_refs_json=entity.skill_refs_json,
+            created_time=entity.created_time)
 
 
 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
     input_json: dict[str, JSONValue] | None = None
 
 
+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
@@ -78,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

+ 114 - 105
services/api-gateway/app/api/routes.py

@@ -1,4 +1,5 @@
 import asyncio
+from typing import Annotated
 
 from core_domain import ServiceDescriptor, ServiceHealth
 from fastapi import APIRouter, Depends, HTTPException, Query, Request, Response
@@ -13,7 +14,9 @@ from app.infrastructure.proxy import ProxyServiceName, ProxyTarget, ServiceProxy
 from app.schemas.gateway import (
     ApiKeyCreateRequest,
     ApiKeyCreateResponse,
+    ApiKeyListRequest,
     ApiKeyResponse,
+    ApiKeyStatusPostRequest,
     ApiKeyStatusUpdateRequest,
     GatewayAuditServiceStats,
     GatewayAuditStatsResponse,
@@ -22,16 +25,17 @@ from app.schemas.gateway import (
 )
 
 router = APIRouter()
+DbSession = Annotated[Session, Depends(get_db)]
 
 
 @router.get("/health", response_model=ServiceDescriptor)
-def health_check(db: Session = Depends(get_db)) -> ServiceDescriptor:
+def health_check(db: DbSession) -> ServiceDescriptor:
     db.execute(text("SELECT 1"))
     return ServiceDescriptor(name="api-gateway")
 
 
 @router.get("/ready", response_model=ServiceHealth)
-def readiness_check(db: Session = Depends(get_db)) -> ServiceHealth:
+def readiness_check(db: DbSession) -> ServiceHealth:
     db.execute(text("SELECT 1"))
     return ServiceHealth(service="api-gateway", status="ok", database="ok")
 
@@ -39,7 +43,7 @@ def readiness_check(db: Session = Depends(get_db)) -> ServiceHealth:
 @router.post("/gateway/api-keys", response_model=ApiKeyCreateResponse)
 def create_api_key(
     payload: ApiKeyCreateRequest,
-    db: Session = Depends(get_db)) -> ApiKeyCreateResponse:
+    db: DbSession) -> ApiKeyCreateResponse:
     api_key = generate_api_key()
     entity = ApiKeyRepository(db).create(
         name=payload.name,
@@ -60,7 +64,17 @@ def create_api_key(
 
 @router.get("/gateway/api-keys", response_model=list[ApiKeyResponse])
 def list_api_keys(
-    db: Session = Depends(get_db)) -> list[ApiKeyResponse]:
+    db: DbSession) -> list[ApiKeyResponse]:
+    return [
+        ApiKeyResponse.from_entity(item)
+        for item in ApiKeyRepository(db).list_all()
+    ]
+
+
+@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()
@@ -71,7 +85,7 @@ def list_api_keys(
 def update_api_key_status(
     api_key_id: str,
     payload: ApiKeyStatusUpdateRequest,
-    db: Session = Depends(get_db)) -> ApiKeyResponse:
+    db: DbSession) -> ApiKeyResponse:
     entity = ApiKeyRepository(db).update_status(
         api_key_id=api_key_id,
         status=payload.status)
@@ -80,12 +94,24 @@ 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(
-    request_id: str | None = Query(default=None),
-    target_service: str | None = Query(default=None),
-    limit: int = Query(default=100, ge=1, le=500),
-    db: Session = Depends(get_db)) -> list[GatewayRequestAuditResponse]:
+    db: DbSession,
+    request_id: Annotated[str | None, Query()] = None,
+    target_service: Annotated[str | None, Query()] = None,
+    limit: Annotated[int, Query(ge=1, le=500)] = 100) -> list[GatewayRequestAuditResponse]:
     items = GatewayRequestAuditRepository(db).list_by_scope(
         request_id=request_id,
         target_service=target_service,
@@ -95,7 +121,7 @@ def list_gateway_audits(
 
 @router.get("/gateway/audits/stats", response_model=GatewayAuditStatsResponse)
 def gateway_audit_stats(
-    db: Session = Depends(get_db)) -> GatewayAuditStatsResponse:
+    db: DbSession) -> GatewayAuditStatsResponse:
     rows = GatewayRequestAuditRepository(db).stats_by_service()
     services = [
         GatewayAuditServiceStats(
@@ -115,27 +141,22 @@ def get_gateway_settings() -> ApiGatewaySettings:
     return ApiGatewaySettings()
 
 
-def get_service_proxy(settings: ApiGatewaySettings = Depends(get_gateway_settings)) -> ServiceProxy:
+def get_service_proxy(
+    settings: Annotated[ApiGatewaySettings, Depends(get_gateway_settings)]) -> ServiceProxy:
     return ServiceProxy(settings=settings, timeout_seconds=settings.proxy_timeout_seconds)
 
 
+GatewaySettingsDep = Annotated[ApiGatewaySettings, Depends(get_gateway_settings)]
+ServiceProxyDep = Annotated[ServiceProxy, Depends(get_service_proxy)]
+
+
 def build_proxy_targets(settings: ApiGatewaySettings) -> dict[ProxyServiceName, ProxyTarget]:
     return {
-        "workflow-service": ProxyTarget(
-            service_name="workflow-service",
-            base_url=settings.workflow_service_url,
-            path_prefix="/workflows",
-            health_path="/workflows/health"),
         "session-service": ProxyTarget(
             service_name="session-service",
             base_url=settings.session_service_url,
             path_prefix="/sessions",
             health_path="/sessions/health"),
-        "runtime-service": ProxyTarget(
-            service_name="runtime-service",
-            base_url=settings.runtime_service_url,
-            path_prefix="/runtime",
-            health_path="/runtime/health"),
         "tool-service": ProxyTarget(
             service_name="tool-service",
             base_url=settings.tool_service_url,
@@ -146,6 +167,11 @@ def build_proxy_targets(settings: ApiGatewaySettings) -> dict[ProxyServiceName,
             base_url=settings.model_gateway_service_url,
             path_prefix="/models",
             health_path="/models/health"),
+        "model-provider-service": ProxyTarget(
+            service_name="model-provider-service",
+            base_url=settings.model_gateway_service_url,
+            path_prefix="/models/providers",
+            health_path="/models/health"),
         "code-runner-service": ProxyTarget(
             service_name="code-runner-service",
             base_url=settings.code_runner_service_url,
@@ -186,11 +212,11 @@ def build_proxy_targets(settings: ApiGatewaySettings) -> dict[ProxyServiceName,
             base_url=settings.event_service_url,
             path_prefix="/events",
             health_path="/events/health"),
-        "auth-service": ProxyTarget(
-            service_name="auth-service",
+        "identity-service": ProxyTarget(
+            service_name="identity-service",
             base_url=settings.auth_service_url,
-            path_prefix="/auth",
-            health_path="/auth/health"),
+            path_prefix="/identity",
+            health_path="/identity/health"),
         "scheduler-service": ProxyTarget(
             service_name="scheduler-service",
             base_url=settings.scheduler_service_url,
@@ -201,7 +227,7 @@ def build_proxy_targets(settings: ApiGatewaySettings) -> dict[ProxyServiceName,
 
 @router.get("/gateway/services/health", response_model=GatewayServicesHealthResponse)
 async def downstream_health_check(
-    settings: ApiGatewaySettings = Depends(get_gateway_settings)) -> GatewayServicesHealthResponse:
+    settings: GatewaySettingsDep) -> GatewayServicesHealthResponse:
     targets = build_proxy_targets(settings)
     health_proxy = ServiceProxy(
         settings=settings,
@@ -215,23 +241,6 @@ async def downstream_health_check(
         downstream_services=downstream_services)
 
 
-@router.api_route(
-    "/gateway/workflows",
-    methods=["GET", "POST", "PUT", "PATCH", "DELETE"])
-@router.api_route(
-    "/gateway/workflows/{path:path}",
-    methods=["GET", "POST", "PUT", "PATCH", "DELETE"])
-async def proxy_workflow_service(
-    request: Request,
-    path: str = "",
-    settings: ApiGatewaySettings = Depends(get_gateway_settings),
-    proxy: ServiceProxy = Depends(get_service_proxy)) -> Response:
-    return await proxy.forward(
-        request=request,
-        target=build_proxy_targets(settings)["workflow-service"],
-        path=path)
-
-
 @router.api_route(
     "/gateway/sessions",
     methods=["GET", "POST", "PUT", "PATCH", "DELETE"])
@@ -240,32 +249,15 @@ async def proxy_workflow_service(
     methods=["GET", "POST", "PUT", "PATCH", "DELETE"])
 async def proxy_session_service(
     request: Request,
-    path: str = "",
-    settings: ApiGatewaySettings = Depends(get_gateway_settings),
-    proxy: ServiceProxy = Depends(get_service_proxy)) -> Response:
+    settings: GatewaySettingsDep,
+    proxy: ServiceProxyDep,
+    path: str = "") -> Response:
     return await proxy.forward(
         request=request,
         target=build_proxy_targets(settings)["session-service"],
         path=path)
 
 
-@router.api_route(
-    "/gateway/runtime",
-    methods=["GET", "POST", "PUT", "PATCH", "DELETE"])
-@router.api_route(
-    "/gateway/runtime/{path:path}",
-    methods=["GET", "POST", "PUT", "PATCH", "DELETE"])
-async def proxy_runtime_service(
-    request: Request,
-    path: str = "",
-    settings: ApiGatewaySettings = Depends(get_gateway_settings),
-    proxy: ServiceProxy = Depends(get_service_proxy)) -> Response:
-    return await proxy.forward(
-        request=request,
-        target=build_proxy_targets(settings)["runtime-service"],
-        path=path)
-
-
 @router.api_route(
     "/gateway/agents",
     methods=["GET", "POST", "PUT", "PATCH", "DELETE"])
@@ -274,9 +266,9 @@ async def proxy_runtime_service(
     methods=["GET", "POST", "PUT", "PATCH", "DELETE"])
 async def proxy_agent_service(
     request: Request,
-    path: str = "",
-    settings: ApiGatewaySettings = Depends(get_gateway_settings),
-    proxy: ServiceProxy = Depends(get_service_proxy)) -> Response:
+    settings: GatewaySettingsDep,
+    proxy: ServiceProxyDep,
+    path: str = "") -> Response:
     return await proxy.forward(
         request=request,
         target=build_proxy_targets(settings)["agent-service"],
@@ -291,9 +283,9 @@ async def proxy_agent_service(
     methods=["GET", "POST", "PUT", "PATCH", "DELETE"])
 async def proxy_memory_service(
     request: Request,
-    path: str = "",
-    settings: ApiGatewaySettings = Depends(get_gateway_settings),
-    proxy: ServiceProxy = Depends(get_service_proxy)) -> Response:
+    settings: GatewaySettingsDep,
+    proxy: ServiceProxyDep,
+    path: str = "") -> Response:
     return await proxy.forward(
         request=request,
         target=build_proxy_targets(settings)["memory-service"],
@@ -308,9 +300,9 @@ async def proxy_memory_service(
     methods=["GET", "POST", "PUT", "PATCH", "DELETE"])
 async def proxy_team_service(
     request: Request,
-    path: str = "",
-    settings: ApiGatewaySettings = Depends(get_gateway_settings),
-    proxy: ServiceProxy = Depends(get_service_proxy)) -> Response:
+    settings: GatewaySettingsDep,
+    proxy: ServiceProxyDep,
+    path: str = "") -> Response:
     return await proxy.forward(
         request=request,
         target=build_proxy_targets(settings)["team-service"],
@@ -325,9 +317,9 @@ async def proxy_team_service(
     methods=["GET", "POST", "PUT", "PATCH", "DELETE"])
 async def proxy_skill_service(
     request: Request,
-    path: str = "",
-    settings: ApiGatewaySettings = Depends(get_gateway_settings),
-    proxy: ServiceProxy = Depends(get_service_proxy)) -> Response:
+    settings: GatewaySettingsDep,
+    proxy: ServiceProxyDep,
+    path: str = "") -> Response:
     return await proxy.forward(
         request=request,
         target=build_proxy_targets(settings)["skill-service"],
@@ -342,9 +334,9 @@ async def proxy_skill_service(
     methods=["GET", "POST", "PUT", "PATCH", "DELETE"])
 async def proxy_human_service(
     request: Request,
-    path: str = "",
-    settings: ApiGatewaySettings = Depends(get_gateway_settings),
-    proxy: ServiceProxy = Depends(get_service_proxy)) -> Response:
+    settings: GatewaySettingsDep,
+    proxy: ServiceProxyDep,
+    path: str = "") -> Response:
     return await proxy.forward(
         request=request,
         target=build_proxy_targets(settings)["human-service"],
@@ -359,9 +351,9 @@ async def proxy_human_service(
     methods=["GET", "POST", "PUT", "PATCH", "DELETE"])
 async def proxy_knowledge_service(
     request: Request,
-    path: str = "",
-    settings: ApiGatewaySettings = Depends(get_gateway_settings),
-    proxy: ServiceProxy = Depends(get_service_proxy)) -> Response:
+    settings: GatewaySettingsDep,
+    proxy: ServiceProxyDep,
+    path: str = "") -> Response:
     return await proxy.forward(
         request=request,
         target=build_proxy_targets(settings)["knowledge-service"],
@@ -376,9 +368,9 @@ async def proxy_knowledge_service(
     methods=["GET", "POST", "PUT", "PATCH", "DELETE"])
 async def proxy_event_service(
     request: Request,
-    path: str = "",
-    settings: ApiGatewaySettings = Depends(get_gateway_settings),
-    proxy: ServiceProxy = Depends(get_service_proxy)) -> Response:
+    settings: GatewaySettingsDep,
+    proxy: ServiceProxyDep,
+    path: str = "") -> Response:
     return await proxy.forward(
         request=request,
         target=build_proxy_targets(settings)["event-service"],
@@ -386,19 +378,19 @@ async def proxy_event_service(
 
 
 @router.api_route(
-    "/gateway/auth",
-    methods=["GET", "POST", "PUT", "PATCH", "DELETE"])
+    "/gateway/identity",
+    methods=["POST"])
 @router.api_route(
-    "/gateway/auth/{path:path}",
-    methods=["GET", "POST", "PUT", "PATCH", "DELETE"])
-async def proxy_auth_service(
+    "/gateway/identity/{path:path}",
+    methods=["POST"])
+async def proxy_identity_service(
     request: Request,
-    path: str = "",
-    settings: ApiGatewaySettings = Depends(get_gateway_settings),
-    proxy: ServiceProxy = Depends(get_service_proxy)) -> Response:
+    settings: GatewaySettingsDep,
+    proxy: ServiceProxyDep,
+    path: str = "") -> Response:
     return await proxy.forward(
         request=request,
-        target=build_proxy_targets(settings)["auth-service"],
+        target=build_proxy_targets(settings)["identity-service"],
         path=path)
 
 
@@ -410,9 +402,9 @@ async def proxy_auth_service(
     methods=["GET", "POST", "PUT", "PATCH", "DELETE"])
 async def proxy_scheduler_service(
     request: Request,
-    path: str = "",
-    settings: ApiGatewaySettings = Depends(get_gateway_settings),
-    proxy: ServiceProxy = Depends(get_service_proxy)) -> Response:
+    settings: GatewaySettingsDep,
+    proxy: ServiceProxyDep,
+    path: str = "") -> Response:
     return await proxy.forward(
         request=request,
         target=build_proxy_targets(settings)["scheduler-service"],
@@ -427,9 +419,9 @@ async def proxy_scheduler_service(
     methods=["GET", "POST", "PUT", "PATCH", "DELETE"])
 async def proxy_tool_service(
     request: Request,
-    path: str = "",
-    settings: ApiGatewaySettings = Depends(get_gateway_settings),
-    proxy: ServiceProxy = Depends(get_service_proxy)) -> Response:
+    settings: GatewaySettingsDep,
+    proxy: ServiceProxyDep,
+    path: str = "") -> Response:
     return await proxy.forward(
         request=request,
         target=build_proxy_targets(settings)["tool-service"],
@@ -444,15 +436,32 @@ async def proxy_tool_service(
     methods=["GET", "POST", "PUT", "PATCH", "DELETE"])
 async def proxy_model_gateway_service(
     request: Request,
-    path: str = "",
-    settings: ApiGatewaySettings = Depends(get_gateway_settings),
-    proxy: ServiceProxy = Depends(get_service_proxy)) -> Response:
+    settings: GatewaySettingsDep,
+    proxy: ServiceProxyDep,
+    path: str = "") -> Response:
     return await proxy.forward(
         request=request,
         target=build_proxy_targets(settings)["model-gateway-service"],
         path=path)
 
 
+@router.api_route(
+    "/gateway/model-providers",
+    methods=["POST"])
+@router.api_route(
+    "/gateway/model-providers/{path:path}",
+    methods=["POST"])
+async def proxy_model_provider_service(
+    request: Request,
+    settings: GatewaySettingsDep,
+    proxy: ServiceProxyDep,
+    path: str = "") -> Response:
+    return await proxy.forward(
+        request=request,
+        target=build_proxy_targets(settings)["model-provider-service"],
+        path=path)
+
+
 @router.api_route(
     "/gateway/code",
     methods=["GET", "POST", "PUT", "PATCH", "DELETE"])
@@ -461,9 +470,9 @@ async def proxy_model_gateway_service(
     methods=["GET", "POST", "PUT", "PATCH", "DELETE"])
 async def proxy_code_runner_service(
     request: Request,
-    path: str = "",
-    settings: ApiGatewaySettings = Depends(get_gateway_settings),
-    proxy: ServiceProxy = Depends(get_service_proxy)) -> Response:
+    settings: GatewaySettingsDep,
+    proxy: ServiceProxyDep,
+    path: str = "") -> Response:
     return await proxy.forward(
         request=request,
         target=build_proxy_targets(settings)["code-runner-service"],

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

@@ -4,10 +4,7 @@ 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"
     tool_service_url: str = "http://127.0.0.1:8004"
     model_gateway_service_url: str = "http://127.0.0.1:8005"
     code_runner_service_url: str = "http://127.0.0.1:8006"

+ 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)

+ 34 - 3
services/api-gateway/app/infrastructure/proxy.py

@@ -5,6 +5,7 @@ import httpx
 from core_shared.observability import PARENT_SPAN_ID_HEADER, SPAN_ID_HEADER, TRACE_ID_HEADER
 from core_shared.security import build_internal_service_headers
 from fastapi import Request, Response
+from fastapi.responses import StreamingResponse
 
 from app.bootstrap.settings import ApiGatewaySettings
 from app.infrastructure.audit import mark_gateway_target
@@ -12,11 +13,10 @@ from app.infrastructure.request_context import REQUEST_ID_HEADER, get_gateway_re
 from app.schemas.gateway import DownstreamServiceHealth
 
 ProxyServiceName = Literal[
-    "workflow-service",
     "session-service",
-    "runtime-service",
     "tool-service",
     "model-gateway-service",
+    "model-provider-service",
     "code-runner-service",
     "agent-service",
     "memory-service",
@@ -25,7 +25,7 @@ ProxyServiceName = Literal[
     "human-service",
     "knowledge-service",
     "event-service",
-    "auth-service",
+    "identity-service",
     "scheduler-service",
 ]
 
@@ -66,6 +66,21 @@ class ServiceProxy:
         headers.update(build_internal_service_headers(self.settings))
         body = await request.body()
 
+        if _expects_stream(request):
+            client = httpx.AsyncClient(timeout=self.timeout_seconds)
+            request_builder = client.build_request(
+                method=request.method,
+                url=target_url,
+                params=request.query_params,
+                headers=headers,
+                content=body)
+            upstream_response = await client.send(request_builder, stream=True)
+            return StreamingResponse(
+                _stream_upstream_response(client, upstream_response),
+                status_code=upstream_response.status_code,
+                headers=build_response_headers(upstream_response),
+                media_type=upstream_response.headers.get("content-type"))
+
         async with httpx.AsyncClient(timeout=self.timeout_seconds) as client:
             upstream_response = await client.request(
                 method=request.method,
@@ -135,3 +150,19 @@ def build_response_headers(response: httpx.Response) -> dict[str, str]:
         for key, value in response.headers.items()
         if key.lower() not in skipped_headers
     }
+
+
+def _expects_stream(request: Request) -> bool:
+    accept_header = request.headers.get("accept", "")
+    return "text/event-stream" in accept_header or request.url.path.endswith("stream")
+
+
+async def _stream_upstream_response(
+    client: httpx.AsyncClient,
+    response: httpx.Response):
+    try:
+        async for chunk in response.aiter_raw():
+            yield chunk
+    finally:
+        await response.aclose()
+        await client.aclose()

+ 22 - 13
services/api-gateway/app/infrastructure/request_context.py

@@ -48,7 +48,6 @@ class GatewayRequestContextMiddleware(BaseHTTPMiddleware):
                 session_factory=request.app.state.session_factory,
                 status_code=auth_response.status_code,
                 error_message=None)
-            context = get_gateway_request_context(request)
             auth_response.headers[REQUEST_ID_HEADER] = request_id
             return auth_response
 
@@ -65,7 +64,6 @@ class GatewayRequestContextMiddleware(BaseHTTPMiddleware):
                 session_factory=request.app.state.session_factory,
                 status_code=rate_limit_response.status_code,
                 error_message="rate limit exceeded")
-            context = get_gateway_request_context(request)
             rate_limit_response.headers[REQUEST_ID_HEADER] = request_id
             return rate_limit_response
 
@@ -87,7 +85,6 @@ class GatewayRequestContextMiddleware(BaseHTTPMiddleware):
             request=request,
             session_factory=request.app.state.session_factory,
             status_code=response.status_code)
-        context = get_gateway_request_context(request)
         response.headers[REQUEST_ID_HEADER] = request_id
         apply_gateway_rate_limit_headers(
             response=response,
@@ -171,7 +168,7 @@ def authenticate_gateway_request(request: Request) -> Response | None:
 
 
 def is_auth_login_request(request: Request) -> bool:
-    return request.method.upper() == "POST" and request.url.path == "/gateway/auth/login"
+    return request.method.upper() == "POST" and request.url.path == "/gateway/identity/auth/login"
 
 
 def is_initial_api_key_bootstrap_request(request: Request) -> bool:
@@ -203,9 +200,9 @@ def authenticate_bearer_token(
     try:
         with httpx.Client(timeout=settings.authz_timeout_seconds) as client:
             response = client.post(
-                f"{settings.auth_service_url.rstrip('/')}/auth/tokens/verify",
+                f"{settings.auth_service_url.rstrip('/')}/identity/auth/tokens/verify",
                 headers=build_internal_service_headers(settings),
-                json={"access_token": token})
+                json={"accessToken": token})
             response.raise_for_status()
             payload = response.json()
     except (httpx.HTTPError, ValueError) as exc:
@@ -213,8 +210,14 @@ def authenticate_bearer_token(
             status_code=503,
             content={"detail": "auth token verification failed", "error": str(exc)})
 
-    if payload.get("active") is not True:
-        reason = payload.get("reason")
+    data = payload.get("data")
+    if not isinstance(data, dict):
+        return JSONResponse(
+            status_code=401,
+            content={"detail": "invalid token verification response"})
+
+    if data.get("active") is not True:
+        reason = data.get("reason")
         return JSONResponse(
             status_code=401,
             content={
@@ -222,7 +225,7 @@ def authenticate_bearer_token(
                 "reason": reason if isinstance(reason, str) else "inactive",
             })
 
-    user_id = payload.get("user_id")
+    user_id = data.get("userId")
     if not isinstance(user_id, str):
         return JSONResponse(
             status_code=401,
@@ -280,10 +283,10 @@ def check_auth_service_permission(
     try:
         with httpx.Client(timeout=settings.authz_timeout_seconds) as client:
             response = client.post(
-                f"{settings.auth_service_url.rstrip('/')}/auth/permissions/check",
+                f"{settings.auth_service_url.rstrip('/')}/identity/permissions/check",
                 headers=build_internal_service_headers(settings),
                 json={
-                    "user_id": user_id,
+                    "userId": user_id,
                     "permission": permission,
                 })
             response.raise_for_status()
@@ -293,10 +296,16 @@ def check_auth_service_permission(
             status_code=503,
             content={"detail": "auth service permission check failed", "error": str(exc)})
 
-    allowed = payload.get("allowed")
+    data = payload.get("data")
+    if not isinstance(data, dict):
+        return JSONResponse(
+            status_code=403,
+            content={"detail": "invalid permission check response"})
+
+    allowed = data.get("allowed")
     if allowed is True:
         return None
-    reason = payload.get("reason")
+    reason = data.get("reason")
     return JSONResponse(
         status_code=403,
         content={

+ 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)

+ 9 - 0
services/auth-service/alembic/versions/20260427_0002_add_user_password_hash.py

@@ -17,6 +17,8 @@ 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=""))
@@ -25,3 +27,10 @@ def upgrade() -> 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

+ 106 - 0
services/auth-service/alembic/versions/20260428_0004_add_identity_contract_tables.py

@@ -0,0 +1,106 @@
+"""add identity contract tables
+
+Revision ID: 20260428_0004
+Revises: 20260427_0003
+Create Date: 2026-04-28 22:35:00
+"""
+
+from collections.abc import Sequence
+
+import sqlalchemy as sa
+from alembic import op
+
+revision: str = "20260428_0004"
+down_revision: str | None = "20260427_0003"
+branch_labels: Sequence[str] | None = None
+depends_on: Sequence[str] | None = None
+
+
+def upgrade() -> None:
+    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"],
+    )
+    _create_index_if_missing(
+        "ix_auth_role_permission_binding_permission",
+        "auth_role_permission_binding",
+        ["permission"],
+    )
+    _create_index_if_missing(
+        "ix_auth_role_permission_binding_scope_type",
+        "auth_role_permission_binding",
+        ["scope_type"],
+    )
+    _create_index_if_missing(
+        "ix_auth_role_permission_binding_scope_id",
+        "auth_role_permission_binding",
+        ["scope_id"],
+    )
+
+    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",
+        ["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

+ 303 - 0
services/auth-service/app/api/identity_routes.py

@@ -0,0 +1,303 @@
+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
+
+from app.application.services import AuthApplicationService
+from app.db.session import get_db
+from app.domain.repositories import (
+    ApiKeyRepository,
+    RoleAssignmentRepository,
+    RolePermissionBindingRepository,
+    RoleRepository,
+    UserRepository,
+)
+from app.schemas.identity import (
+    ApiKeyCreateData,
+    ApiKeyCreateRequestDto,
+    ApiKeyDto,
+    ApiKeyRevokeRequest,
+    ApiResponse,
+    AuthMeData,
+    BindingRemoveRequest,
+    DeleteData,
+    LoginData,
+    LoginRequestDto,
+    PageRequest,
+    PageResult,
+    PermissionCheckData,
+    PermissionCheckRequestDto,
+    RoleDto,
+    RolePermissionBindingAddRequest,
+    RolePermissionBindingDto,
+    RolePermissionBindingListRequest,
+    TokenVerifyData,
+    TokenVerifyRequestDto,
+    UserDto,
+)
+
+router = APIRouter()
+DbSession = Annotated[Session, Depends(get_db)]
+T = TypeVar("T")
+
+
+def get_identity_application_service(request: Request, db: DbSession) -> AuthApplicationService:
+    settings = request.app.state.settings
+    return AuthApplicationService(
+        user_repository=UserRepository(db),
+        role_repository=RoleRepository(db),
+        assignment_repository=RoleAssignmentRepository(db),
+        permission_binding_repository=RolePermissionBindingRepository(db),
+        api_key_repository=ApiKeyRepository(db),
+        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)]
+AuthorizationHeader = Annotated[str | None, Header(alias="Authorization")]
+
+
+def ok(request: Request, data: T) -> ApiResponse[T]:
+    return ApiResponse[T](
+        data=data,
+        requestId=request.headers.get("x-request-id", ""),
+        serverTime=datetime.utcnow())
+
+
+def get_bearer_token(authorization: str | None) -> str:
+    if not authorization:
+        raise HTTPException(status_code=401, detail="missing authorization header")
+    scheme, _, token = authorization.partition(" ")
+    if scheme.lower() != "bearer" or not token:
+        raise HTTPException(status_code=401, detail="invalid authorization header")
+    return token
+
+
+@router.get("/health", response_model=ServiceHealth)
+def health_check(db: DbSession) -> ServiceHealth:
+    db.execute(text("SELECT 1"))
+    return ServiceHealth(service="identity-service", status="ok", database="ok")
+
+
+@router.post("/auth/login", response_model=ApiResponse[LoginData])
+def login(
+    request: Request,
+    payload: LoginRequestDto,
+    service: IdentityServiceDep) -> ApiResponse[LoginData]:
+    result = service.login(username=payload.username, password=payload.password)
+    if result is None:
+        raise HTTPException(status_code=401, detail="invalid username or password")
+    return ok(
+        request,
+        LoginData(
+            accessToken=result.access_token,
+            expiresTime=result.expires_time,
+            user=UserDto.from_entity(result.user)))
+
+
+@router.post("/auth/logout", response_model=ApiResponse[dict[str, bool]])
+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])
+def verify_token(
+    request: Request,
+    payload: TokenVerifyRequestDto,
+    service: IdentityServiceDep) -> ApiResponse[TokenVerifyData]:
+    result = service.verify_token(access_token=payload.accessToken)
+    return ok(
+        request,
+        TokenVerifyData(
+            active=result.active,
+            userId=result.user_id,
+            username=result.username,
+            expiresTime=result.expires_time,
+            reason=result.reason))
+
+
+@router.post("/auth/me", response_model=ApiResponse[AuthMeData])
+def me(
+    request: Request,
+    service: IdentityServiceDep,
+    authorization: AuthorizationHeader = None) -> ApiResponse[AuthMeData]:
+    token = get_bearer_token(authorization)
+    verified = service.verify_token(access_token=token)
+    if not verified.active or verified.user_id is None:
+        raise HTTPException(status_code=401, detail=verified.reason or "invalid token")
+    user = service.user_repository.get_by_id(user_id=verified.user_id)
+    if user is None:
+        raise HTTPException(status_code=401, detail="user not found")
+
+    assignments = service.assignment_repository.list_by_user(user_id=user.id)
+    roles = []
+    permissions: set[str] = set()
+    for assignment in assignments:
+        role = service.role_repository.get_by_id(role_id=assignment.role_id)
+        if role is None:
+            continue
+        bindings = service.permission_binding_repository.list_all_by_role(role_id=role.id)
+        roles.append(RoleDto.from_entity(role, permission_binding_count=len(bindings)))
+        permissions.update(binding.permission for binding in bindings)
+    return ok(
+        request,
+        AuthMeData(
+            user=UserDto.from_entity(user),
+            roles=roles,
+            permissions=sorted(permissions)))
+
+
+@router.post("/users/list", response_model=ApiResponse[PageResult[UserDto]])
+def list_users(
+    request: Request,
+    payload: PageRequest,
+    service: IdentityServiceDep) -> ApiResponse[PageResult[UserDto]]:
+    items, total = service.list_users_page(
+        page=payload.page,
+        page_size=payload.pageSize,
+        keyword=payload.keyword)
+    return ok(
+        request,
+        PageResult[UserDto].from_items(
+            items=[UserDto.from_entity(item) for item in items],
+            total=total,
+            page=payload.page,
+            page_size=payload.pageSize))
+
+
+@router.post("/roles/list", response_model=ApiResponse[PageResult[RoleDto]])
+def list_roles(
+    request: Request,
+    payload: PageRequest,
+    service: IdentityServiceDep) -> ApiResponse[PageResult[RoleDto]]:
+    items, total = service.list_roles_page(
+        page=payload.page,
+        page_size=payload.pageSize,
+        keyword=payload.keyword)
+    binding_repo = service.permission_binding_repository
+    return ok(
+        request,
+        PageResult[RoleDto].from_items(
+            items=[
+                RoleDto.from_entity(
+                    item,
+                    permission_binding_count=len(binding_repo.list_all_by_role(role_id=item.id)))
+                for item in items
+            ],
+            total=total,
+            page=payload.page,
+            page_size=payload.pageSize))
+
+
+@router.post(
+    "/rolePermissionBindings/list",
+    response_model=ApiResponse[PageResult[RolePermissionBindingDto]])
+def list_role_permission_bindings(
+    request: Request,
+    payload: RolePermissionBindingListRequest,
+    service: IdentityServiceDep) -> ApiResponse[PageResult[RolePermissionBindingDto]]:
+    items, total = service.list_role_permission_bindings(
+        role_id=payload.roleId,
+        page=payload.page,
+        page_size=payload.pageSize)
+    return ok(
+        request,
+        PageResult[RolePermissionBindingDto].from_items(
+            items=[RolePermissionBindingDto.from_entity(item) for item in items],
+            total=total,
+            page=payload.page,
+            page_size=payload.pageSize))
+
+
+@router.post("/rolePermissionBindings/add", response_model=ApiResponse[RolePermissionBindingDto])
+def add_role_permission_binding(
+    request: Request,
+    payload: RolePermissionBindingAddRequest,
+    service: IdentityServiceDep) -> ApiResponse[RolePermissionBindingDto]:
+    entity = service.add_role_permission_binding(
+        role_id=payload.roleId,
+        permission=payload.permission,
+        scope_type=payload.scopeType,
+        scope_id=payload.scopeId)
+    return ok(request, RolePermissionBindingDto.from_entity(entity))
+
+
+@router.post("/rolePermissionBindings/remove", response_model=ApiResponse[DeleteData])
+def remove_role_permission_binding(
+    request: Request,
+    payload: BindingRemoveRequest,
+    service: IdentityServiceDep) -> ApiResponse[DeleteData]:
+    deleted = service.remove_role_permission_binding(binding_id=payload.bindingId)
+    return ok(request, DeleteData(deleted=deleted, bindingId=payload.bindingId))
+
+
+@router.post("/permissions/check", response_model=ApiResponse[PermissionCheckData])
+def check_permission(
+    request: Request,
+    payload: PermissionCheckRequestDto,
+    service: IdentityServiceDep) -> ApiResponse[PermissionCheckData]:
+    result = service.check_permission(
+        user_id=payload.userId,
+        permission=payload.permission,
+        scope_type=payload.scopeType,
+        scope_id=payload.scopeId)
+    return ok(
+        request,
+        PermissionCheckData(
+            allowed=result.allowed,
+            reason=result.reason,
+            matchedRoleIds=result.matched_role_ids))
+
+
+@router.post("/apiKeys/list", response_model=ApiResponse[PageResult[ApiKeyDto]])
+def list_api_keys(
+    request: Request,
+    payload: PageRequest,
+    service: IdentityServiceDep) -> ApiResponse[PageResult[ApiKeyDto]]:
+    items, total = service.list_api_keys_page(
+        page=payload.page,
+        page_size=payload.pageSize,
+        keyword=payload.keyword)
+    return ok(
+        request,
+        PageResult[ApiKeyDto].from_items(
+            items=[ApiKeyDto.from_entity(item) for item in items],
+            total=total,
+            page=payload.page,
+            page_size=payload.pageSize))
+
+
+@router.post("/apiKeys/create", response_model=ApiResponse[ApiKeyCreateData])
+def create_api_key(
+    request: Request,
+    payload: ApiKeyCreateRequestDto,
+    service: IdentityServiceDep) -> ApiResponse[ApiKeyCreateData]:
+    entity, secret = service.create_api_key(
+        name=payload.name,
+        scopes=payload.scopes,
+        expires_time=payload.expiresTime)
+    return ok(
+        request,
+        ApiKeyCreateData(
+            apiKey=ApiKeyDto.from_entity(entity),
+            secret=secret))
+
+
+@router.post("/apiKeys/revoke", response_model=ApiResponse[ApiKeyDto])
+def revoke_api_key(
+    request: Request,
+    payload: ApiKeyRevokeRequest,
+    service: IdentityServiceDep) -> ApiResponse[ApiKeyDto]:
+    entity = service.revoke_api_key(api_key_id=payload.apiKeyId)
+    if entity is None:
+        raise HTTPException(status_code=404, detail=f"api key not found: {payload.apiKeyId}")
+    return ok(request, ApiKeyDto.from_entity(entity))

+ 0 - 149
services/auth-service/app/api/routes.py

@@ -1,149 +0,0 @@
-from typing import Annotated
-
-from core_domain import ServiceHealth
-from fastapi import APIRouter, Depends, HTTPException, Query, Request
-from sqlalchemy import text
-from sqlalchemy.orm import Session
-
-from app.application.services import AuthApplicationService
-from app.db.session import get_db
-from app.domain.repositories import RoleAssignmentRepository, RoleRepository, UserRepository
-from app.schemas.auth import (
-    LoginRequest,
-    LoginResponse,
-    PermissionCheckRequest,
-    PermissionCheckResponse,
-    RoleAssignmentCreateRequest,
-    RoleAssignmentResponse,
-    RoleAssignmentStatusUpdateRequest,
-    RoleCreateRequest,
-    RoleResponse,
-    RoleStatusUpdateRequest,
-    TokenVerifyRequest,
-    TokenVerifyResponse,
-    UserCreateRequest,
-    UserResponse,
-    UserStatusUpdateRequest,
-)
-
-router = APIRouter()
-DbSession = Annotated[Session, Depends(get_db)]
-UserIdQuery = Annotated[str, Query(...)]
-
-
-def get_auth_application_service(request: Request, db: DbSession) -> AuthApplicationService:
-    settings = request.app.state.settings
-    return AuthApplicationService(
-        user_repository=UserRepository(db),
-        role_repository=RoleRepository(db),
-        assignment_repository=RoleAssignmentRepository(db),
-        token_secret=settings.credential_encryption_key)
-
-
-AuthServiceDep = Annotated[AuthApplicationService, Depends(get_auth_application_service)]
-
-
-@router.get("/health", response_model=ServiceHealth)
-def health_check(db: DbSession) -> ServiceHealth:
-    db.execute(text("SELECT 1"))
-    return ServiceHealth(service="auth-service", status="ok", database="ok")
-
-
-@router.post("/login", response_model=LoginResponse)
-def login(
-    payload: LoginRequest,
-    service: AuthServiceDep) -> LoginResponse:
-    result = service.login(payload)
-    if result is None:
-        raise HTTPException(status_code=401, detail="invalid username or password")
-    return result
-
-
-@router.post("/tokens/verify", response_model=TokenVerifyResponse)
-def verify_token(
-    payload: TokenVerifyRequest,
-    service: AuthServiceDep) -> TokenVerifyResponse:
-    return service.verify_token(payload)
-
-
-@router.post("/users", response_model=UserResponse)
-def create_user(
-    payload: UserCreateRequest,
-    service: AuthServiceDep) -> UserResponse:
-    return UserResponse.from_entity(service.create_user(payload))
-
-
-@router.get("/users", response_model=list[UserResponse])
-def list_users(
-    service: AuthServiceDep) -> list[UserResponse]:
-    return [UserResponse.from_entity(item) for item in service.list_users()]
-
-
-@router.patch("/users/{user_id}/status", response_model=UserResponse)
-def update_user_status(
-    user_id: str,
-    payload: UserStatusUpdateRequest,
-    service: AuthServiceDep) -> UserResponse:
-    entity = service.update_user_status(user_id=user_id, payload=payload)
-    if entity is None:
-        raise HTTPException(status_code=404, detail=f"user not found: {user_id}")
-    return UserResponse.from_entity(entity)
-
-
-@router.post("/roles", response_model=RoleResponse)
-def create_role(
-    payload: RoleCreateRequest,
-    service: AuthServiceDep) -> RoleResponse:
-    return RoleResponse.from_entity(service.create_role(payload))
-
-
-@router.get("/roles", response_model=list[RoleResponse])
-def list_roles(
-    service: AuthServiceDep) -> list[RoleResponse]:
-    return [RoleResponse.from_entity(item) for item in service.list_roles()]
-
-
-@router.patch("/roles/{role_id}/status", response_model=RoleResponse)
-def update_role_status(
-    role_id: str,
-    payload: RoleStatusUpdateRequest,
-    service: AuthServiceDep) -> RoleResponse:
-    entity = service.update_role_status(role_id=role_id, payload=payload)
-    if entity is None:
-        raise HTTPException(status_code=404, detail=f"role not found: {role_id}")
-    return RoleResponse.from_entity(entity)
-
-
-@router.post("/assignments", response_model=RoleAssignmentResponse)
-def create_assignment(
-    payload: RoleAssignmentCreateRequest,
-    service: AuthServiceDep) -> RoleAssignmentResponse:
-    return RoleAssignmentResponse.from_entity(service.create_assignment(payload))
-
-
-@router.get("/assignments", response_model=list[RoleAssignmentResponse])
-def list_assignments(
-    user_id: UserIdQuery,
-    service: AuthServiceDep) -> list[RoleAssignmentResponse]:
-    return [
-        RoleAssignmentResponse.from_entity(item)
-        for item in service.list_assignments(user_id=user_id)
-    ]
-
-
-@router.patch("/assignments/{assignment_id}/status", response_model=RoleAssignmentResponse)
-def update_assignment_status(
-    assignment_id: str,
-    payload: RoleAssignmentStatusUpdateRequest,
-    service: AuthServiceDep) -> RoleAssignmentResponse:
-    entity = service.update_assignment_status(assignment_id=assignment_id, payload=payload)
-    if entity is None:
-        raise HTTPException(status_code=404, detail=f"assignment not found: {assignment_id}")
-    return RoleAssignmentResponse.from_entity(entity)
-
-
-@router.post("/permissions/check", response_model=PermissionCheckResponse)
-def check_permission(
-    payload: PermissionCheckRequest,
-    service: AuthServiceDep) -> PermissionCheckResponse:
-    return service.check_permission(payload)

+ 315 - 85
services/auth-service/app/application/services.py

@@ -1,24 +1,42 @@
+import hashlib
+import json
+from dataclasses import dataclass
 from datetime import datetime
 
-from app.db.models import Role, RoleAssignment, User
-from app.domain.repositories import RoleAssignmentRepository, RoleRepository, UserRepository
-from app.infrastructure.passwords import hash_password, verify_password
-from app.infrastructure.tokens import TokenError, issue_access_token, verify_access_token
-from app.schemas.auth import (
-    LoginRequest,
-    LoginResponse,
-    LoginUserResponse,
-    PermissionCheckRequest,
-    PermissionCheckResponse,
-    RoleAssignmentCreateRequest,
-    RoleAssignmentStatusUpdateRequest,
-    RoleCreateRequest,
-    RoleStatusUpdateRequest,
-    TokenVerifyRequest,
-    TokenVerifyResponse,
-    UserCreateRequest,
-    UserStatusUpdateRequest,
+from app.db.models import ApiKey, Role, RoleAssignment, RolePermissionBinding, User
+from app.domain.repositories import (
+    ApiKeyRepository,
+    RoleAssignmentRepository,
+    RolePermissionBindingRepository,
+    RoleRepository,
+    UserRepository,
 )
+from app.infrastructure.api_keys import generate_api_key, get_api_key_prefix, hash_api_key
+from app.infrastructure.passwords import verify_password
+from app.infrastructure.tokens import TokenError, issue_access_token, verify_access_token
+
+
+@dataclass(frozen=True)
+class LoginResult:
+    access_token: str
+    expires_time: datetime
+    user: User
+
+
+@dataclass(frozen=True)
+class TokenVerificationResult:
+    active: bool
+    user_id: str | None = None
+    username: str | None = None
+    expires_time: datetime | None = None
+    reason: str | None = None
+
+
+@dataclass(frozen=True)
+class PermissionCheckResult:
+    allowed: bool
+    reason: str
+    matched_role_ids: list[str]
 
 
 class AuthApplicationService:
@@ -28,110 +46,205 @@ class AuthApplicationService:
         user_repository: UserRepository,
         role_repository: RoleRepository,
         assignment_repository: RoleAssignmentRepository,
-        token_secret: str) -> None:
+        permission_binding_repository: RolePermissionBindingRepository,
+        api_key_repository: ApiKeyRepository,
+        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 create_user(self, payload: UserCreateRequest) -> User:
-        return self.user_repository.create(
-            username=payload.username,
-            password_hash=hash_password(payload.password) if payload.password else "",
-            display_name=payload.display_name,
-            email=payload.email,
-            metadata_json=payload.metadata_json)
-
-    def login(self, payload: LoginRequest) -> LoginResponse | None:
-        user = self.user_repository.get_by_username(
-            username=payload.username)
+    def login(self, *, username: str, password: str) -> LoginResult | None:
+        user = self.user_repository.get_by_username(username=username)
         if user is None or user.status != "active":
             return None
-        if not verify_password(payload.password, user.password_hash):
+        if not verify_password(password, user.password_hash):
             return None
 
         self.user_repository.touch_last_login_time(user_id=user.id)
         access_token, expires_time = issue_access_token(
             user_id=user.id,
             secret=self.token_secret)
-        return LoginResponse(
+        return LoginResult(
             access_token=access_token,
             expires_time=expires_time,
-            user=LoginUserResponse.from_entity(user))
+            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")
 
-    def verify_token(self, payload: TokenVerifyRequest) -> TokenVerifyResponse:
         try:
-            token_payload = verify_access_token(
-                payload.access_token,
-                secret=self.token_secret)
+            token_payload = verify_access_token(access_token, secret=self.token_secret)
         except TokenError as exc:
-            return TokenVerifyResponse(active=False, reason=str(exc))
+            return TokenVerificationResult(active=False, reason=str(exc))
 
         expires_time_raw = token_payload["expires_time"]
-        user = self.user_repository.get_by_id(
-            user_id=token_payload["user_id"])
+        user = self.user_repository.get_by_id(user_id=token_payload["user_id"])
         if user is None or user.status != "active":
-            return TokenVerifyResponse(active=False, reason="user_not_active")
+            return TokenVerificationResult(active=False, reason="user_not_active")
 
-        return TokenVerifyResponse(
+        return TokenVerificationResult(
             active=True,
             user_id=user.id,
             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()
 
-    def update_user_status(self, *, user_id: str, payload: UserStatusUpdateRequest) -> User | None:
-        return self.user_repository.update_status(
-            user_id=user_id,
-            status=payload.status)
-
-    def create_role(self, payload: RoleCreateRequest) -> Role:
-        return self.role_repository.create(
-            code=payload.code,
-            name=payload.name,
-            description=payload.description,
-            permissions_json=payload.permissions_json)
+    def list_users_page(
+        self,
+        *,
+        page: int,
+        page_size: int,
+        keyword: str | None) -> tuple[list[User], int]:
+        return self.user_repository.list_page(
+            offset=(page - 1) * page_size,
+            limit=page_size,
+            keyword=keyword)
 
     def list_roles(self) -> list[Role]:
         return self.role_repository.list_all()
 
-    def update_role_status(self, *, role_id: str, payload: RoleStatusUpdateRequest) -> Role | None:
-        return self.role_repository.update_status(
-            role_id=role_id,
-            status=payload.status)
-
-    def create_assignment(
+    def list_roles_page(
         self,
-        payload: RoleAssignmentCreateRequest) -> RoleAssignment:
-        return self.assignment_repository.create(
-            user_id=payload.user_id,
-            role_id=payload.role_id,
-            scope_type=payload.scope_type,
-            scope_id=payload.scope_id,
-            expires_time=payload.expires_time)
+        *,
+        page: int,
+        page_size: int,
+        keyword: str | None) -> tuple[list[Role], int]:
+        return self.role_repository.list_page(
+            offset=(page - 1) * page_size,
+            limit=page_size,
+            keyword=keyword)
 
     def list_assignments(self, *, user_id: str) -> list[RoleAssignment]:
         return self.assignment_repository.list_by_user(user_id=user_id)
 
-    def update_assignment_status(
+    def list_role_permission_bindings(
+        self,
+        *,
+        role_id: str,
+        page: int,
+        page_size: int) -> tuple[list[RolePermissionBinding], int]:
+        return self.permission_binding_repository.list_by_role(
+            role_id=role_id,
+            offset=(page - 1) * page_size,
+            limit=page_size)
+
+    def add_role_permission_binding(
+        self,
+        *,
+        role_id: str,
+        permission: str,
+        scope_type: str | None,
+        scope_id: str | None) -> RolePermissionBinding:
+        return self.permission_binding_repository.create(
+            role_id=role_id,
+            permission=permission,
+            scope_type=scope_type,
+            scope_id=scope_id)
+
+    def remove_role_permission_binding(self, *, binding_id: str) -> bool:
+        return self.permission_binding_repository.delete(binding_id=binding_id)
+
+    def list_api_keys_page(
+        self,
+        *,
+        page: int,
+        page_size: int,
+        keyword: str | None) -> tuple[list[ApiKey], int]:
+        return self.api_key_repository.list_page(
+            offset=(page - 1) * page_size,
+            limit=page_size,
+            keyword=keyword)
+
+    def create_api_key(
+        self,
+        *,
+        name: str,
+        scopes: str | None,
+        expires_time: datetime | None) -> tuple[ApiKey, str]:
+        secret = generate_api_key()
+        entity = self.api_key_repository.create(
+            name=name,
+            key_prefix=get_api_key_prefix(secret),
+            key_hash=hash_api_key(secret),
+            scopes=scopes,
+            expires_time=expires_time)
+        return entity, secret
+
+    def revoke_api_key(self, *, api_key_id: str) -> ApiKey | None:
+        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,
         *,
-        assignment_id: str,
-        payload: RoleAssignmentStatusUpdateRequest) -> RoleAssignment | None:
-        return self.assignment_repository.update_status(
-            assignment_id=assignment_id,
-            status=payload.status)
-
-    def check_permission(self, payload: PermissionCheckRequest) -> PermissionCheckResponse:
-        user = self.user_repository.get_by_id(
-            user_id=payload.user_id)
+        user_id: str,
+        permission: str,
+        scope_type: str | None,
+        scope_id: str | None) -> PermissionCheckResult:
+        user = self.user_repository.get_by_id(user_id=user_id)
         if user is None or user.status != "active":
-            return PermissionCheckResponse(allowed=False, reason="user_not_active")
+            return PermissionCheckResult(
+                allowed=False,
+                reason="user_not_active",
+                matched_role_ids=[])
 
-        assignments = self.assignment_repository.list_by_user(
-            user_id=payload.user_id)
+        assignments = self.assignment_repository.list_by_user(user_id=user_id)
         matched_role_ids: list[str] = []
         now = datetime.utcnow()
         for assignment in assignments:
@@ -141,21 +254,122 @@ class AuthApplicationService:
                 continue
             if not self._scope_matches(
                 assignment=assignment,
-                scope_type=payload.scope_type,
-                scope_id=payload.scope_id):
+                scope_type=scope_type,
+                scope_id=scope_id):
                 continue
-            role = self.role_repository.get_by_id(
-                role_id=assignment.role_id)
+            role = self.role_repository.get_by_id(role_id=assignment.role_id)
             if role is None or role.status != "active":
                 continue
-            if self._permission_matches(role.permissions_json, payload.permission):
+            if self._role_has_permission(
+                role,
+                permission,
+                scope_type=scope_type,
+                scope_id=scope_id):
                 matched_role_ids.append(role.id)
 
-        return PermissionCheckResponse(
+        return PermissionCheckResult(
             allowed=bool(matched_role_ids),
             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
@@ -174,3 +388,19 @@ class AuthApplicationService:
         if assignment.scope_type is None and assignment.scope_id is None:
             return True
         return assignment.scope_type == scope_type and assignment.scope_id == scope_id
+
+    def _role_has_permission(
+        self,
+        role: Role,
+        requested_permission: str,
+        *,
+        scope_type: str | None,
+        scope_id: str | None) -> bool:
+        bindings = self.permission_binding_repository.list_all_by_role(role_id=role.id)
+        return any(
+            (binding.scope_type is None and binding.scope_id is None
+             or (binding.scope_type == scope_type and binding.scope_id == scope_id))
+            and
+            self._permission_matches([binding.permission], requested_permission)
+            for binding in bindings
+        )

+ 4 - 2
services/auth-service/app/bootstrap/app.py

@@ -2,7 +2,8 @@ from core_shared.observability import add_observability
 from core_shared.security import add_internal_service_auth
 from fastapi import FastAPI
 
-from app.api.routes import router
+from app.api.identity_routes import router as identity_router
+from app.bootstrap.demo_seed import bootstrap_demo_identity
 from app.bootstrap.settings import AuthServiceSettings
 from app.db.session import build_session_factory
 
@@ -14,7 +15,8 @@ def create_app() -> FastAPI:
         version="0.1.0")
     app.state.settings = settings
     app.state.session_factory = build_session_factory(settings)
+    bootstrap_demo_identity(settings=settings, session_factory=app.state.session_factory)
     add_observability(app, settings.service_name)
     add_internal_service_auth(app, settings)
-    app.include_router(router, prefix="/auth", tags=["auth"])
+    app.include_router(identity_router, prefix="/identity", tags=["identity"])
     return app

+ 62 - 0
services/auth-service/app/bootstrap/demo_seed.py

@@ -0,0 +1,62 @@
+import logging
+
+from sqlalchemy.exc import SQLAlchemyError
+from sqlalchemy.orm import sessionmaker
+
+from app.bootstrap.settings import AuthServiceSettings
+from app.domain.repositories import (
+    RoleAssignmentRepository,
+    RolePermissionBindingRepository,
+    RoleRepository,
+    UserRepository,
+)
+from app.infrastructure.passwords import hash_password
+
+logger = logging.getLogger(__name__)
+
+
+def bootstrap_demo_identity(
+    *,
+    settings: AuthServiceSettings,
+    session_factory: sessionmaker) -> None:
+    if not settings.demo_user_bootstrap_enabled or settings.service_env != "local":
+        return
+
+    db = session_factory()
+    try:
+        users = UserRepository(db)
+        if users.has_any():
+            return
+
+        user = users.create(
+            username=settings.demo_user_username,
+            password_hash=hash_password(settings.demo_user_password),
+            display_name=settings.demo_user_display_name,
+            email=settings.demo_user_email,
+            metadata_json={"source": "local-bootstrap"})
+
+        roles = RoleRepository(db)
+        role = roles.get_by_name(name="Administrator")
+        if role is None:
+            role = roles.create(
+                code="administrator",
+                name="Administrator",
+                description="Local bootstrap administrator",
+                permissions_json=[])
+
+        RoleAssignmentRepository(db).create(
+            user_id=user.id,
+            role_id=role.id,
+            scope_type=None,
+            scope_id=None,
+            expires_time=None)
+        RolePermissionBindingRepository(db).create(
+            role_id=role.id,
+            permission="*",
+            scope_type=None,
+            scope_id=None)
+    except SQLAlchemyError as exc:
+        db.rollback()
+        logger.warning("Skipped demo identity bootstrap: %s", exc)
+    finally:
+        db.close()

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

@@ -4,6 +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

+ 3 - 1
services/auth-service/app/db/models/__init__.py

@@ -1,7 +1,9 @@
 from core_db import Base
 
+from .api_key import ApiKey
 from .role import Role
 from .role_assignment import RoleAssignment
+from .role_permission_binding import RolePermissionBinding
 from .user import User
 
-__all__ = ["Base", "Role", "RoleAssignment", "User"]
+__all__ = ["ApiKey", "Base", "Role", "RoleAssignment", "RolePermissionBinding", "User"]

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

@@ -0,0 +1,17 @@
+from datetime import datetime
+
+from core_db import AuditMixin, Base, EntityMixin
+from sqlalchemy import DateTime, String, Text
+from sqlalchemy.orm import Mapped, mapped_column
+
+
+class ApiKey(EntityMixin, AuditMixin, Base):
+    __tablename__ = "auth_api_key"
+
+    name: Mapped[str] = mapped_column(String(128))
+    key_prefix: Mapped[str] = mapped_column(String(16), index=True)
+    key_hash: Mapped[str] = mapped_column(String(128), unique=True, index=True)
+    scopes: Mapped[str | None] = mapped_column(Text, nullable=True)
+    expires_time: Mapped[datetime | None] = mapped_column(DateTime, nullable=True)
+    last_used_time: Mapped[datetime | None] = mapped_column(DateTime, nullable=True)
+    revoked_time: Mapped[datetime | None] = mapped_column(DateTime, nullable=True, index=True)

+ 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()))

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

@@ -0,0 +1,15 @@
+from uuid import uuid4
+
+from core_db import AuditMixin, Base
+from sqlalchemy import String
+from sqlalchemy.orm import Mapped, mapped_column
+
+
+class RolePermissionBinding(Base, AuditMixin):
+    __tablename__ = "auth_role_permission_binding"
+
+    id: Mapped[str] = mapped_column(String(36), primary_key=True, default=lambda: str(uuid4()))
+    role_id: Mapped[str] = mapped_column(String(36), index=True)
+    permission: Mapped[str] = mapped_column(String(256), index=True)
+    scope_type: Mapped[str | None] = mapped_column(String(64), nullable=True, index=True)
+    scope_id: Mapped[str | None] = mapped_column(String(64), nullable=True, index=True)

+ 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()))

+ 133 - 1
services/auth-service/app/domain/repositories.py

@@ -5,7 +5,7 @@ from core_shared import JSONValue
 from sqlalchemy import select
 from sqlalchemy.orm import Session
 
-from app.db.models import Role, RoleAssignment, User
+from app.db.models import ApiKey, Role, RoleAssignment, RolePermissionBinding, User
 
 
 class UserRepository:
@@ -35,6 +35,23 @@ class UserRepository:
         stmt = select(User).order_by(User.created_time.desc())
         return list(self.db.scalars(stmt))
 
+    def has_any(self) -> bool:
+        stmt = select(User.id).limit(1)
+        return self.db.scalar(stmt) is not None
+
+    def list_page(self, *, offset: int, limit: int, keyword: str | None) -> tuple[list[User], int]:
+        stmt = select(User)
+        if keyword:
+            like = f"%{keyword}%"
+            stmt = stmt.where(
+                (User.username.like(like))
+                | (User.display_name.like(like))
+                | (User.email.like(like))
+            )
+        total = len(list(self.db.scalars(stmt)))
+        items_stmt = stmt.order_by(User.created_time.desc()).offset(offset).limit(limit)
+        return list(self.db.scalars(items_stmt)), total
+
     def get_by_id(self, *, user_id: str) -> User | None:
         return self.db.get(User, user_id)
 
@@ -85,6 +102,19 @@ class RoleRepository:
         stmt = select(Role).order_by(Role.created_time.desc())
         return list(self.db.scalars(stmt))
 
+    def get_by_name(self, *, name: str) -> Role | None:
+        stmt = select(Role).where(Role.name == name)
+        return self.db.scalar(stmt)
+
+    def list_page(self, *, offset: int, limit: int, keyword: str | None) -> tuple[list[Role], int]:
+        stmt = select(Role)
+        if keyword:
+            like = f"%{keyword}%"
+            stmt = stmt.where((Role.name.like(like)) | (Role.description.like(like)))
+        total = len(list(self.db.scalars(stmt)))
+        items_stmt = stmt.order_by(Role.created_time.desc()).offset(offset).limit(limit)
+        return list(self.db.scalars(items_stmt)), total
+
     def get_by_id(self, *, role_id: str) -> Role | None:
         return self.db.get(Role, role_id)
 
@@ -147,3 +177,105 @@ class RoleAssignmentRepository:
         self.db.commit()
         self.db.refresh(entity)
         return entity
+
+
+class RolePermissionBindingRepository:
+    def __init__(self, db: Session) -> None:
+        self.db = db
+
+    def create(
+        self,
+        *,
+        role_id: str,
+        permission: str,
+        scope_type: str | None,
+        scope_id: str | None) -> RolePermissionBinding:
+        entity = RolePermissionBinding(
+            role_id=role_id,
+            permission=permission,
+            scope_type=scope_type,
+            scope_id=scope_id)
+        self.db.add(entity)
+        self.db.commit()
+        self.db.refresh(entity)
+        return entity
+
+    def list_by_role(
+        self,
+        *,
+        role_id: str,
+        offset: int = 0,
+        limit: int = 100) -> tuple[list[RolePermissionBinding], int]:
+        stmt = select(RolePermissionBinding).where(RolePermissionBinding.role_id == role_id)
+        total = len(list(self.db.scalars(stmt)))
+        items_stmt = (
+            stmt.order_by(RolePermissionBinding.created_time.desc())
+            .offset(offset)
+            .limit(limit)
+        )
+        return list(self.db.scalars(items_stmt)), total
+
+    def list_all_by_role(self, *, role_id: str) -> list[RolePermissionBinding]:
+        stmt = (
+            select(RolePermissionBinding)
+            .where(RolePermissionBinding.role_id == role_id)
+            .order_by(RolePermissionBinding.created_time.desc())
+        )
+        return list(self.db.scalars(stmt))
+
+    def delete(self, *, binding_id: str) -> bool:
+        entity = self.db.get(RolePermissionBinding, binding_id)
+        if entity is None:
+            return False
+        self.db.delete(entity)
+        self.db.commit()
+        return True
+
+
+class ApiKeyRepository:
+    def __init__(self, db: Session) -> None:
+        self.db = db
+
+    def create(
+        self,
+        *,
+        name: str,
+        key_prefix: str,
+        key_hash: str,
+        scopes: str | None,
+        expires_time: datetime | None) -> ApiKey:
+        entity = ApiKey(
+            name=name,
+            key_prefix=key_prefix,
+            key_hash=key_hash,
+            scopes=scopes,
+            expires_time=expires_time)
+        self.db.add(entity)
+        self.db.commit()
+        self.db.refresh(entity)
+        return entity
+
+    def list_page(
+        self,
+        *,
+        offset: int,
+        limit: int,
+        keyword: str | None) -> tuple[list[ApiKey], int]:
+        stmt = select(ApiKey)
+        if keyword:
+            stmt = stmt.where(ApiKey.name.like(f"%{keyword}%"))
+        total = len(list(self.db.scalars(stmt)))
+        items_stmt = stmt.order_by(ApiKey.created_time.desc()).offset(offset).limit(limit)
+        return list(self.db.scalars(items_stmt)), total
+
+    def get_by_id(self, *, api_key_id: str) -> ApiKey | None:
+        return self.db.get(ApiKey, api_key_id)
+
+    def revoke(self, *, api_key_id: str) -> ApiKey | None:
+        entity = self.get_by_id(api_key_id=api_key_id)
+        if entity is None:
+            return None
+        entity.revoked_time = datetime.utcnow()
+        self.db.commit()
+        self.db.refresh(entity)
+        return entity

+ 18 - 0
services/auth-service/app/infrastructure/api_keys.py

@@ -0,0 +1,18 @@
+import hashlib
+import secrets
+
+API_KEY_PREFIX = "agp"
+
+
+def generate_api_key() -> str:
+    return f"{API_KEY_PREFIX}_{secrets.token_urlsafe(32)}"
+
+
+def hash_api_key(api_key: str) -> str:
+    return hashlib.sha256(api_key.encode("utf-8")).hexdigest()
+
+
+def get_api_key_prefix(api_key: str) -> str:
+    if len(api_key) <= 12:
+        return api_key
+    return api_key[:12]

+ 0 - 118
services/auth-service/app/schemas/auth.py

@@ -1,118 +0,0 @@
-from datetime import datetime
-from typing import TYPE_CHECKING
-
-from core_domain import (
-    PermissionCheckContract,
-    PermissionCheckResultContract,
-    RoleAssignmentContract,
-    RoleAssignmentStatus,
-    RoleContract,
-    RoleStatus,
-    UserContract,
-    UserStatus,
-)
-from core_shared import JSONValue
-from pydantic import BaseModel, Field
-
-if TYPE_CHECKING:
-    from app.db.models import Role, RoleAssignment, User
-
-
-class UserCreateRequest(BaseModel):
-    username: str
-    password: str | None = Field(default=None, min_length=8)
-    display_name: str | None = None
-    email: str | None = None
-    metadata_json: dict[str, JSONValue] = Field(default_factory=dict)
-
-
-class UserStatusUpdateRequest(BaseModel):
-    status: UserStatus
-
-
-class UserResponse(UserContract):
-    @classmethod
-    def from_entity(cls, entity: "User") -> "UserResponse":
-        return cls.model_validate(entity, from_attributes=True)
-
-
-class LoginUserResponse(BaseModel):
-    id: str
-    username: str
-    display_name: str | None = None
-    email: str | None = None
-    status: UserStatus
-    metadata_json: dict[str, JSONValue] = Field(default_factory=dict)
-    last_login_time: datetime | None = None
-    created_time: datetime
-
-    @classmethod
-    def from_entity(cls, entity: "User") -> "LoginUserResponse":
-        return cls.model_validate(entity, from_attributes=True)
-
-
-class LoginRequest(BaseModel):
-    username: str
-    password: str
-
-
-class LoginResponse(BaseModel):
-    access_token: str
-    token_type: str = "bearer"
-    expires_time: datetime
-    user: LoginUserResponse
-
-
-class TokenVerifyRequest(BaseModel):
-    access_token: str
-
-
-class TokenVerifyResponse(BaseModel):
-    active: bool
-    user_id: str | None = None
-    username: str | None = None
-    expires_time: datetime | None = None
-    reason: str | None = None
-
-
-class RoleCreateRequest(BaseModel):
-    code: str
-    name: str
-    description: str | None = None
-    permissions_json: list[str] = Field(default_factory=list)
-
-
-class RoleStatusUpdateRequest(BaseModel):
-    status: RoleStatus
-
-
-class RoleResponse(RoleContract):
-    @classmethod
-    def from_entity(cls, entity: "Role") -> "RoleResponse":
-        return cls.model_validate(entity, from_attributes=True)
-
-
-class RoleAssignmentCreateRequest(BaseModel):
-    user_id: str
-    role_id: str
-    scope_type: str | None = None
-    scope_id: str | None = None
-    expires_time: datetime | None = None
-
-
-class RoleAssignmentStatusUpdateRequest(BaseModel):
-    status: RoleAssignmentStatus
-
-
-class RoleAssignmentResponse(RoleAssignmentContract):
-    @classmethod
-    def from_entity(cls, entity: "RoleAssignment") -> "RoleAssignmentResponse":
-        return cls.model_validate(entity, from_attributes=True)
-
-
-class PermissionCheckRequest(PermissionCheckContract):
-    pass
-
-
-class PermissionCheckResponse(PermissionCheckResultContract):
-    pass

+ 222 - 0
services/auth-service/app/schemas/identity.py

@@ -0,0 +1,222 @@
+from datetime import datetime
+from typing import TYPE_CHECKING, Generic, TypeVar
+
+from core_shared import JSONValue
+from pydantic import BaseModel, Field
+
+if TYPE_CHECKING:
+    from app.db.models import ApiKey, Role, RolePermissionBinding, User
+
+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
+    sortBy: str = "createdTime"
+    sortOrder: str = "desc"
+
+    @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 UserDto(BaseModel):
+    id: str
+    username: str
+    displayName: str | None = None
+    email: str | None = None
+    metadata: dict[str, JSONValue] = Field(default_factory=dict)
+    lastLoginTime: datetime | None = None
+    createdTime: datetime
+    updatedTime: datetime
+
+    @classmethod
+    def from_entity(cls, entity: "User") -> "UserDto":
+        return cls(
+            id=entity.id,
+            username=entity.username,
+            displayName=entity.display_name,
+            email=entity.email,
+            metadata=entity.metadata_json or {},
+            lastLoginTime=entity.last_login_time,
+            createdTime=entity.created_time,
+            updatedTime=entity.updated_time)
+
+
+class RoleDto(BaseModel):
+    id: str
+    name: str
+    description: str | None = None
+    permissionBindingCount: int
+    createdTime: datetime
+    updatedTime: datetime
+
+    @classmethod
+    def from_entity(cls, entity: "Role", *, permission_binding_count: int = 0) -> "RoleDto":
+        return cls(
+            id=entity.id,
+            name=entity.name,
+            description=entity.description,
+            permissionBindingCount=permission_binding_count,
+            createdTime=entity.created_time,
+            updatedTime=entity.updated_time)
+
+
+class RolePermissionBindingDto(BaseModel):
+    id: str
+    roleId: str
+    permission: str
+    scopeType: str | None = None
+    scopeId: str | None = None
+    createdTime: datetime
+
+    @classmethod
+    def from_entity(cls, entity: "RolePermissionBinding") -> "RolePermissionBindingDto":
+        return cls(
+            id=entity.id,
+            roleId=entity.role_id,
+            permission=entity.permission,
+            scopeType=entity.scope_type,
+            scopeId=entity.scope_id,
+            createdTime=entity.created_time)
+
+
+class ApiKeyDto(BaseModel):
+    id: str
+    name: str
+    keyPrefix: str
+    scopes: str | None = None
+    expiresTime: datetime | None = None
+    lastUsedTime: datetime | None = None
+    revokedTime: datetime | None = None
+    createdTime: datetime
+
+    @classmethod
+    def from_entity(cls, entity: "ApiKey") -> "ApiKeyDto":
+        return cls(
+            id=entity.id,
+            name=entity.name,
+            keyPrefix=entity.key_prefix,
+            scopes=entity.scopes,
+            expiresTime=entity.expires_time,
+            lastUsedTime=entity.last_used_time,
+            revokedTime=entity.revoked_time,
+            createdTime=entity.created_time)
+
+
+class LoginRequestDto(BaseModel):
+    username: str
+    password: str
+
+
+class LoginData(BaseModel):
+    accessToken: str
+    tokenType: str = "bearer"
+    expiresTime: datetime
+    user: UserDto
+
+
+class TokenVerifyRequestDto(BaseModel):
+    accessToken: str
+
+
+class TokenVerifyData(BaseModel):
+    active: bool
+    userId: str | None = None
+    username: str | None = None
+    expiresTime: datetime | None = None
+    reason: str | None = None
+
+
+class AuthMeData(BaseModel):
+    user: UserDto
+    roles: list[RoleDto]
+    permissions: list[str]
+
+
+class RolePermissionBindingAddRequest(BaseModel):
+    roleId: str
+    permission: str
+    scopeType: str | None = None
+    scopeId: str | None = None
+
+
+class RolePermissionBindingListRequest(PageRequest):
+    roleId: str
+
+
+class BindingRemoveRequest(BaseModel):
+    bindingId: str
+
+
+class PermissionCheckRequestDto(BaseModel):
+    userId: str
+    permission: str
+    scopeType: str | None = None
+    scopeId: str | None = None
+
+
+class PermissionCheckData(BaseModel):
+    allowed: bool
+    reason: str
+    matchedRoleIds: list[str] = Field(default_factory=list)
+
+
+class ApiKeyCreateRequestDto(BaseModel):
+    name: str
+    scopes: str | None = None
+    expiresTime: datetime | None = None
+
+
+class ApiKeyCreateData(BaseModel):
+    apiKey: ApiKeyDto
+    secret: str
+
+
+class ApiKeyRevokeRequest(BaseModel):
+    apiKeyId: str
+
+
+class DeleteData(BaseModel):
+    deleted: bool
+    bindingId: str | None = None
+    apiKeyId: str | None = None

+ 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()
 

+ 2 - 0
services/knowledge-service/alembic/versions/20260425_0001_init_knowledge_models.py

@@ -71,7 +71,9 @@ def upgrade() -> None:
         ["content_hash"],
         unique=False)
     op.create_index(
+        "ix_knowledge_document_base_status",
         "knowledge_document",
+        ["knowledge_base_id", "status"],
         unique=False)
 
     op.create_table(

Một số tệp đã không được hiển thị bởi vì quá nhiều tập tin thay đổi trong này khác