Просмотр исходного кода

feat: add agent definition service

Jax Docker 2 месяцев назад
Родитель
Сommit
9399fa1a1e
32 измененных файлов с 1167 добавлено и 0 удалено
  1. 37 0
      README.md
  2. 27 0
      deployments/docker/docker-compose.yml
  3. 22 0
      libs/core-domain/src/core_domain/__init__.py
  4. 88 0
      libs/core-domain/src/core_domain/agent_contracts.py
  5. 1 0
      pyproject.toml
  6. 36 0
      services/agent-service/alembic.ini
  7. 41 0
      services/agent-service/alembic/env.py
  8. 1 0
      services/agent-service/alembic/versions/.gitkeep
  9. 119 0
      services/agent-service/alembic/versions/20260424_0001_init_agent_models.py
  10. 1 0
      services/agent-service/app/__init__.py
  11. 1 0
      services/agent-service/app/api/__init__.py
  12. 133 0
      services/agent-service/app/api/routes.py
  13. 1 0
      services/agent-service/app/application/__init__.py
  14. 146 0
      services/agent-service/app/application/services.py
  15. 1 0
      services/agent-service/app/bootstrap/__init__.py
  16. 17 0
      services/agent-service/app/bootstrap/app.py
  17. 7 0
      services/agent-service/app/bootstrap/settings.py
  18. 1 0
      services/agent-service/app/db/__init__.py
  19. 7 0
      services/agent-service/app/db/models/__init__.py
  20. 18 0
      services/agent-service/app/db/models/agent_definition.py
  21. 26 0
      services/agent-service/app/db/models/agent_run.py
  22. 24 0
      services/agent-service/app/db/models/agent_version.py
  23. 29 0
      services/agent-service/app/db/session.py
  24. 1 0
      services/agent-service/app/domain/__init__.py
  25. 225 0
      services/agent-service/app/domain/repositories.py
  26. 3 0
      services/agent-service/app/main.py
  27. 1 0
      services/agent-service/app/schemas/__init__.py
  28. 98 0
      services/agent-service/app/schemas/agent.py
  29. 26 0
      services/agent-service/pyproject.toml
  30. 27 0
      services/api-gateway/app/api/routes.py
  31. 1 0
      services/api-gateway/app/bootstrap/settings.py
  32. 1 0
      services/api-gateway/app/infrastructure/proxy.py

+ 37 - 0
README.md

@@ -16,6 +16,7 @@
 - `session-service`
 - `workflow-service`
 - `runtime-service`
+- `agent-service`
 - `tool-service`
 
 每个服务都提供了最小 `FastAPI` 启动入口和健康检查接口,数据库相关服务也已经带上了 `SQLAlchemy` 模型骨架与 Alembic 目录。
@@ -45,6 +46,7 @@ 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\tool-service
 ```
 
@@ -212,6 +214,39 @@ tests/
 
 ## Runtime Execute APIs
 
+## Agent Service APIs
+
+`agent-service` stores strongly typed agent definitions, versioned prompts/configuration, and agent run records.
+
+Create an agent:
+
+```powershell
+Invoke-RestMethod -Method Post `
+  -Uri http://127.0.0.1:8007/agents `
+  -ContentType "application/json" `
+  -Body '{"tenant_id":"t1","code":"sales_agent","name":"Sales Agent","agent_type":"assistant"}'
+```
+
+Create a published agent version:
+
+```powershell
+Invoke-RestMethod -Method Post `
+  -Uri http://127.0.0.1:8007/agents/versions `
+  -ContentType "application/json" `
+  -Body '{"tenant_id":"t1","agent_id":"agent-id","status":"published","role":"sales_assistant","goal":"Help qualify leads","system_prompt":"You are a careful sales assistant."}'
+```
+
+Create an agent run. If `agent_version_id` is omitted, the latest published version is used:
+
+```powershell
+Invoke-RestMethod -Method Post `
+  -Uri http://127.0.0.1:8007/agents/runs `
+  -ContentType "application/json" `
+  -Body '{"tenant_id":"t1","agent_id":"agent-id","session_id":"session-id","input_text":"Summarize this lead."}'
+```
+
+Through `api-gateway`, use `/gateway/agents/**`.
+
 `runtime-service` now includes a typed executor skeleton for these node types:
 
 - `llm`
@@ -449,6 +484,7 @@ $env:AGENT_PLATFORM_SMOKE_RUNTIME_URL="http://127.0.0.1:8000/gateway/runtime"
 - `/gateway/workflows/**` -> `workflow-service /workflows/**`
 - `/gateway/sessions/**` -> `session-service /sessions/**`
 - `/gateway/runtime/**` -> `runtime-service /runtime/**`
+- `/gateway/agents/**` -> `agent-service /agents/**`
 - `/gateway/tools/**` -> `tool-service /tools/**`
 - `/gateway/models/**` -> `model-gateway-service /models/**`
 - `/gateway/code/**` -> `code-runner-service /code/**`
@@ -674,6 +710,7 @@ docker compose -f .\deployments\docker\docker-compose.yml down
 Important notes:
 
 - `workflow-service`, `session-service`, `runtime-service`, `tool-service`, and `api-gateway` use SQLite files mounted under `/data`
+- `agent-service` stores agent definitions, prompt/config versions, and agent run records under `/data`
 - `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

+ 27 - 0
deployments/docker/docker-compose.yml

@@ -102,6 +102,26 @@ services:
       timeout: 5s
       retries: 5
 
+  agent-service:
+    build:
+      context: ../..
+      dockerfile: deployments/docker/python-service.Dockerfile
+      args:
+        SERVICE_PATH: services/agent-service
+    container_name: agent-platform-agent-service
+    command: ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8007"]
+    environment:
+      AGENT_PLATFORM_DATABASE_URL: sqlite:////data/agent_service.db
+    ports:
+      - "8007:8007"
+    volumes:
+      - agent_service_data:/data
+    healthcheck:
+      test: ["CMD", "python", "-c", "import urllib.request; urllib.request.urlopen('http://127.0.0.1:8007/agents/health').read()"]
+      interval: 15s
+      timeout: 5s
+      retries: 5
+
   runtime-service:
     build:
       context: ../..
@@ -116,6 +136,7 @@ services:
       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
     ports:
       - "8003:8003"
     volumes:
@@ -129,6 +150,8 @@ services:
         condition: service_started
       code-runner-service:
         condition: service_started
+      agent-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
@@ -178,6 +201,7 @@ services:
       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_AUTH_REQUIRED: ${AGENT_PLATFORM_AUTH_REQUIRED:-false}
     ports:
       - "8000:8000"
@@ -196,6 +220,8 @@ services:
         condition: service_started
       code-runner-service:
         condition: service_started
+      agent-service:
+        condition: service_started
     healthcheck:
       test: ["CMD", "python", "-c", "import urllib.request; urllib.request.urlopen('http://127.0.0.1:8000/health').read()"]
       interval: 15s
@@ -204,6 +230,7 @@ services:
 
 volumes:
   api_gateway_data:
+  agent_service_data:
   workflow_service_data:
   session_service_data:
   runtime_service_data:

+ 22 - 0
libs/core-domain/src/core_domain/__init__.py

@@ -1,3 +1,15 @@
+from .agent_contracts import (
+    AgentDefinitionContract,
+    AgentMemoryPolicyContract,
+    AgentModelConfigContract,
+    AgentRunContract,
+    AgentRunStatus,
+    AgentSkillRefContract,
+    AgentStatus,
+    AgentToolRefContract,
+    AgentVersionContract,
+    AgentVersionStatus,
+)
 from .code_contracts import CodeExecutionRequestContract, CodeExecutionResponseContract
 from .execution_contracts import (
     NodeExecutionContextContract,
@@ -31,6 +43,16 @@ from .tool_contracts import (
 from .workflow_contracts import WorkflowVersionContract
 
 __all__ = [
+    "AgentDefinitionContract",
+    "AgentMemoryPolicyContract",
+    "AgentModelConfigContract",
+    "AgentRunContract",
+    "AgentRunStatus",
+    "AgentSkillRefContract",
+    "AgentStatus",
+    "AgentToolRefContract",
+    "AgentVersionContract",
+    "AgentVersionStatus",
     "CodeExecutionRequestContract",
     "CodeExecutionResponseContract",
     "ChatCompletionRequestContract",

+ 88 - 0
libs/core-domain/src/core_domain/agent_contracts.py

@@ -0,0 +1,88 @@
+from datetime import datetime
+from typing import Literal
+
+from pydantic import BaseModel, Field
+
+from core_shared import JSONValue
+
+
+AgentStatus = Literal["draft", "active", "archived"]
+AgentVersionStatus = Literal["draft", "published", "deprecated"]
+AgentRunStatus = Literal["queued", "running", "completed", "failed", "cancelled"]
+
+
+class AgentModelConfigContract(BaseModel):
+    provider: str | None = None
+    model: str | None = None
+    temperature: float | None = None
+    max_tokens: int | None = None
+    extra_json: dict[str, JSONValue] = Field(default_factory=dict)
+
+
+class AgentToolRefContract(BaseModel):
+    tool_binding_id: str | None = None
+    tool_code: str | None = None
+    required: bool = False
+    config_json: dict[str, JSONValue] = Field(default_factory=dict)
+
+
+class AgentSkillRefContract(BaseModel):
+    skill_id: str | None = None
+    skill_code: str | None = None
+    config_json: dict[str, JSONValue] = Field(default_factory=dict)
+
+
+class AgentMemoryPolicyContract(BaseModel):
+    enabled: bool = True
+    memory_scope: str = "session"
+    read_top_k: int = 8
+    write_enabled: bool = True
+    config_json: dict[str, JSONValue] = Field(default_factory=dict)
+
+
+class AgentDefinitionContract(BaseModel):
+    id: str
+    tenant_id: str
+    code: str
+    name: str
+    description: str | None = None
+    agent_type: str
+    status: AgentStatus
+    owner_user_id: str | None = None
+    created_time: datetime
+
+
+class AgentVersionContract(BaseModel):
+    id: str
+    tenant_id: str
+    agent_id: str
+    version_no: int
+    status: AgentVersionStatus
+    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]]
+    published_time: datetime | None = None
+    created_time: datetime
+
+
+class AgentRunContract(BaseModel):
+    id: str
+    tenant_id: str
+    agent_id: str
+    agent_version_id: str
+    session_id: str | None = None
+    input_text: str | None = None
+    input_json: dict[str, JSONValue] | None = None
+    output_text: str | None = None
+    output_json: dict[str, JSONValue] | None = None
+    status: AgentRunStatus
+    worker_key: str | None = None
+    started_time: datetime | None = None
+    finished_time: datetime | None = None
+    error_code: str | None = None
+    error_message: str | None = None
+    created_time: datetime

+ 1 - 0
pyproject.toml

@@ -6,6 +6,7 @@ members = [
   "libs/core-events",
   "libs/core-shared",
   "services/api-gateway",
+  "services/agent-service",
   "services/code-runner-service",
   "services/model-gateway-service",
   "services/session-service",

+ 36 - 0
services/agent-service/alembic.ini

@@ -0,0 +1,36 @@
+[alembic]
+script_location = alembic
+prepend_sys_path = .
+sqlalchemy.url = sqlite:///./agent_service.db
+
+[loggers]
+keys = root,sqlalchemy,alembic
+
+[handlers]
+keys = console
+
+[formatters]
+keys = generic
+
+[logger_root]
+level = WARN
+handlers = console
+
+[logger_sqlalchemy]
+level = WARN
+handlers =
+qualname = sqlalchemy.engine
+
+[logger_alembic]
+level = INFO
+handlers = console
+qualname = alembic
+
+[handler_console]
+class = StreamHandler
+args = (sys.stderr,)
+level = NOTSET
+formatter = generic
+
+[formatter_generic]
+format = %(levelname)-5.5s [%(name)s] %(message)s

+ 41 - 0
services/agent-service/alembic/env.py

@@ -0,0 +1,41 @@
+from logging.config import fileConfig
+
+from alembic import context
+from sqlalchemy import engine_from_config, pool
+
+from app.db.models import Base
+
+config = context.config
+
+if config.config_file_name is not None:
+    fileConfig(config.config_file_name)
+
+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)
+
+    with context.begin_transaction():
+        context.run_migrations()
+
+
+def run_migrations_online() -> None:
+    connectable = engine_from_config(
+        config.get_section(config.config_ini_section, {}),
+        prefix="sqlalchemy.",
+        poolclass=pool.NullPool,
+    )
+
+    with connectable.connect() as connection:
+        context.configure(connection=connection, target_metadata=target_metadata)
+
+        with context.begin_transaction():
+            context.run_migrations()
+
+
+if context.is_offline_mode():
+    run_migrations_offline()
+else:
+    run_migrations_online()

+ 1 - 0
services/agent-service/alembic/versions/.gitkeep

@@ -0,0 +1 @@
+

+ 119 - 0
services/agent-service/alembic/versions/20260424_0001_init_agent_models.py

@@ -0,0 +1,119 @@
+"""init agent models
+
+Revision ID: 20260424_0001
+Revises:
+Create Date: 2026-04-24 10:20:00
+"""
+
+from collections.abc import Sequence
+
+from alembic import op
+import sqlalchemy as sa
+
+
+revision: str = "20260424_0001"
+down_revision: str | None = None
+branch_labels: Sequence[str] | None = None
+depends_on: Sequence[str] | None = None
+
+
+def upgrade() -> None:
+    op.create_table(
+        "agent_definition",
+        sa.Column("code", sa.String(length=64), nullable=False),
+        sa.Column("name", sa.String(length=128), nullable=False),
+        sa.Column("description", sa.Text(), nullable=True),
+        sa.Column("agent_type", sa.String(length=32), nullable=False),
+        sa.Column("status", sa.String(length=32), nullable=False),
+        sa.Column("owner_user_id", sa.String(length=36), nullable=True),
+        sa.Column("metadata_json", sa.JSON(), nullable=True),
+        sa.Column("id", sa.String(length=36), nullable=False),
+        sa.Column("tenant_id", sa.String(length=36), nullable=False),
+        sa.Column("created_by", sa.String(length=36), nullable=True),
+        sa.Column("updated_by", sa.String(length=36), nullable=True),
+        sa.Column("created_time", sa.DateTime(), nullable=False),
+        sa.Column("updated_time", sa.DateTime(), nullable=False),
+        sa.Column("deleted_time", sa.DateTime(), nullable=True),
+        sa.Column("version", sa.Integer(), nullable=False),
+        sa.PrimaryKeyConstraint("id"),
+    )
+    op.create_index("ix_agent_definition_code", "agent_definition", ["code"], unique=False)
+    op.create_index("ix_agent_definition_status", "agent_definition", ["status"], unique=False)
+    op.create_index("ix_agent_definition_tenant_id", "agent_definition", ["tenant_id"], unique=False)
+
+    op.create_table(
+        "agent_version",
+        sa.Column("agent_id", sa.String(length=36), nullable=False),
+        sa.Column("version_no", sa.Integer(), nullable=False),
+        sa.Column("status", sa.String(length=32), nullable=False),
+        sa.Column("role", sa.String(length=64), nullable=False),
+        sa.Column("goal", sa.Text(), nullable=True),
+        sa.Column("system_prompt", sa.Text(), nullable=False),
+        sa.Column("model_config_json", sa.JSON(), nullable=False),
+        sa.Column("memory_policy_json", sa.JSON(), nullable=False),
+        sa.Column("tool_refs_json", sa.JSON(), nullable=False),
+        sa.Column("skill_refs_json", sa.JSON(), nullable=False),
+        sa.Column("published_time", sa.DateTime(), nullable=True),
+        sa.Column("id", sa.String(length=36), nullable=False),
+        sa.Column("tenant_id", sa.String(length=36), nullable=False),
+        sa.Column("created_by", sa.String(length=36), nullable=True),
+        sa.Column("updated_by", sa.String(length=36), nullable=True),
+        sa.Column("created_time", sa.DateTime(), nullable=False),
+        sa.Column("updated_time", sa.DateTime(), nullable=False),
+        sa.Column("deleted_time", sa.DateTime(), nullable=True),
+        sa.Column("version", sa.Integer(), nullable=False),
+        sa.PrimaryKeyConstraint("id"),
+    )
+    op.create_index("ix_agent_version_agent_id", "agent_version", ["agent_id"], unique=False)
+    op.create_index("ix_agent_version_status", "agent_version", ["status"], unique=False)
+    op.create_index("ix_agent_version_tenant_id", "agent_version", ["tenant_id"], unique=False)
+
+    op.create_table(
+        "agent_run",
+        sa.Column("agent_id", sa.String(length=36), nullable=False),
+        sa.Column("agent_version_id", sa.String(length=36), nullable=False),
+        sa.Column("session_id", sa.String(length=36), nullable=True),
+        sa.Column("status", sa.String(length=32), nullable=False),
+        sa.Column("worker_key", sa.String(length=128), nullable=True),
+        sa.Column("input_text", sa.Text(), nullable=True),
+        sa.Column("input_json", sa.JSON(), nullable=True),
+        sa.Column("output_text", sa.Text(), nullable=True),
+        sa.Column("output_json", sa.JSON(), nullable=True),
+        sa.Column("started_time", sa.DateTime(), nullable=True),
+        sa.Column("finished_time", sa.DateTime(), nullable=True),
+        sa.Column("error_code", sa.String(length=64), nullable=True),
+        sa.Column("error_message", sa.Text(), nullable=True),
+        sa.Column("id", sa.String(length=36), nullable=False),
+        sa.Column("tenant_id", sa.String(length=36), nullable=False),
+        sa.Column("created_by", sa.String(length=36), nullable=True),
+        sa.Column("updated_by", sa.String(length=36), nullable=True),
+        sa.Column("created_time", sa.DateTime(), nullable=False),
+        sa.Column("updated_time", sa.DateTime(), nullable=False),
+        sa.Column("deleted_time", sa.DateTime(), nullable=True),
+        sa.Column("version", sa.Integer(), nullable=False),
+        sa.PrimaryKeyConstraint("id"),
+    )
+    op.create_index("ix_agent_run_agent_id", "agent_run", ["agent_id"], unique=False)
+    op.create_index("ix_agent_run_agent_version_id", "agent_run", ["agent_version_id"], unique=False)
+    op.create_index("ix_agent_run_session_id", "agent_run", ["session_id"], unique=False)
+    op.create_index("ix_agent_run_status", "agent_run", ["status"], unique=False)
+    op.create_index("ix_agent_run_tenant_id", "agent_run", ["tenant_id"], unique=False)
+
+
+def downgrade() -> None:
+    op.drop_index("ix_agent_run_tenant_id", table_name="agent_run")
+    op.drop_index("ix_agent_run_status", table_name="agent_run")
+    op.drop_index("ix_agent_run_session_id", table_name="agent_run")
+    op.drop_index("ix_agent_run_agent_version_id", table_name="agent_run")
+    op.drop_index("ix_agent_run_agent_id", table_name="agent_run")
+    op.drop_table("agent_run")
+
+    op.drop_index("ix_agent_version_tenant_id", table_name="agent_version")
+    op.drop_index("ix_agent_version_status", table_name="agent_version")
+    op.drop_index("ix_agent_version_agent_id", table_name="agent_version")
+    op.drop_table("agent_version")
+
+    op.drop_index("ix_agent_definition_tenant_id", table_name="agent_definition")
+    op.drop_index("ix_agent_definition_status", table_name="agent_definition")
+    op.drop_index("ix_agent_definition_code", table_name="agent_definition")
+    op.drop_table("agent_definition")

+ 1 - 0
services/agent-service/app/__init__.py

@@ -0,0 +1 @@
+

+ 1 - 0
services/agent-service/app/api/__init__.py

@@ -0,0 +1 @@
+

+ 133 - 0
services/agent-service/app/api/routes.py

@@ -0,0 +1,133 @@
+from fastapi import APIRouter, Depends, HTTPException, Query
+from sqlalchemy import text
+from sqlalchemy.orm import Session
+
+from core_domain import ServiceHealth
+
+from app.application.services import AgentApplicationService
+from app.db.session import get_db
+from app.domain.repositories import (
+    AgentDefinitionRepository,
+    AgentRunRepository,
+    AgentVersionRepository,
+)
+from app.schemas.agent import (
+    AgentCreateRequest,
+    AgentResponse,
+    AgentRunCreateRequest,
+    AgentRunResponse,
+    AgentRunStatusUpdateRequest,
+    AgentStatusUpdateRequest,
+    AgentVersionCreateRequest,
+    AgentVersionResponse,
+)
+
+router = APIRouter()
+
+
+def get_agent_application_service(db: Session = Depends(get_db)) -> AgentApplicationService:
+    return AgentApplicationService(
+        agent_repository=AgentDefinitionRepository(db),
+        agent_version_repository=AgentVersionRepository(db),
+        agent_run_repository=AgentRunRepository(db),
+    )
+
+
+@router.get("/health", response_model=ServiceHealth)
+def health_check(db: Session = Depends(get_db)) -> ServiceHealth:
+    db.execute(text("SELECT 1"))
+    return ServiceHealth(service="agent-service", status="ok", database="ok")
+
+
+@router.post("", response_model=AgentResponse)
+def create_agent(
+    payload: AgentCreateRequest,
+    service: AgentApplicationService = Depends(get_agent_application_service),
+) -> AgentResponse:
+    entity = service.create_agent(payload)
+    return AgentResponse.from_entity(entity)
+
+
+@router.get("", response_model=list[AgentResponse])
+def list_agents(
+    tenant_id: str = Query(...),
+    service: AgentApplicationService = Depends(get_agent_application_service),
+) -> list[AgentResponse]:
+    return [AgentResponse.from_entity(item) for item in service.list_agents(tenant_id=tenant_id)]
+
+
+@router.patch("/{agent_id}/status", response_model=AgentResponse)
+def update_agent_status(
+    agent_id: str,
+    payload: AgentStatusUpdateRequest,
+    service: AgentApplicationService = Depends(get_agent_application_service),
+) -> AgentResponse:
+    entity = service.update_agent_status(agent_id=agent_id, payload=payload)
+    if entity is None:
+        raise HTTPException(status_code=404, detail=f"agent not found: {agent_id}")
+    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:
+    try:
+        entity = service.create_agent_version(payload)
+    except ValueError as exc:
+        raise HTTPException(status_code=422, detail=str(exc)) from exc
+    return AgentVersionResponse.from_entity(entity)
+
+
+@router.get("/versions", response_model=list[AgentVersionResponse])
+def list_agent_versions(
+    tenant_id: str = Query(...),
+    agent_id: str = Query(...),
+    service: AgentApplicationService = Depends(get_agent_application_service),
+) -> list[AgentVersionResponse]:
+    return [
+        AgentVersionResponse.from_entity(item)
+        for item in service.list_agent_versions(tenant_id=tenant_id, agent_id=agent_id)
+    ]
+
+
+@router.post("/runs", response_model=AgentRunResponse)
+def create_agent_run(
+    payload: AgentRunCreateRequest,
+    service: AgentApplicationService = Depends(get_agent_application_service),
+) -> AgentRunResponse:
+    try:
+        entity = service.create_agent_run(payload)
+    except ValueError as exc:
+        raise HTTPException(status_code=422, detail=str(exc)) from exc
+    return AgentRunResponse.from_entity(entity)
+
+
+@router.get("/runs", response_model=list[AgentRunResponse])
+def list_agent_runs(
+    tenant_id: str = Query(...),
+    agent_id: str | None = Query(default=None),
+    session_id: str | None = Query(default=None),
+    service: AgentApplicationService = Depends(get_agent_application_service),
+) -> list[AgentRunResponse]:
+    return [
+        AgentRunResponse.from_entity(item)
+        for item in service.list_agent_runs(
+            tenant_id=tenant_id,
+            agent_id=agent_id,
+            session_id=session_id,
+        )
+    ]
+
+
+@router.post("/runs/{agent_run_id}/status", response_model=AgentRunResponse)
+def update_agent_run_status(
+    agent_run_id: str,
+    payload: AgentRunStatusUpdateRequest,
+    service: AgentApplicationService = Depends(get_agent_application_service),
+) -> AgentRunResponse:
+    entity = service.update_agent_run_status(agent_run_id=agent_run_id, payload=payload)
+    if entity is None:
+        raise HTTPException(status_code=404, detail=f"agent_run not found: {agent_run_id}")
+    return AgentRunResponse.from_entity(entity)

+ 1 - 0
services/agent-service/app/application/__init__.py

@@ -0,0 +1 @@
+

+ 146 - 0
services/agent-service/app/application/services.py

@@ -0,0 +1,146 @@
+from app.db.models import AgentDefinition, AgentRun, AgentVersion
+from app.domain.repositories import (
+    AgentDefinitionRepository,
+    AgentRunRepository,
+    AgentVersionRepository,
+)
+from app.schemas.agent import (
+    AgentCreateRequest,
+    AgentRunCreateRequest,
+    AgentRunStatusUpdateRequest,
+    AgentStatusUpdateRequest,
+    AgentVersionCreateRequest,
+)
+
+
+class AgentApplicationService:
+    def __init__(
+        self,
+        *,
+        agent_repository: AgentDefinitionRepository,
+        agent_version_repository: AgentVersionRepository,
+        agent_run_repository: AgentRunRepository,
+    ) -> None:
+        self.agent_repository = agent_repository
+        self.agent_version_repository = agent_version_repository
+        self.agent_run_repository = agent_run_repository
+
+    def create_agent(self, payload: AgentCreateRequest) -> AgentDefinition:
+        return self.agent_repository.create(
+            tenant_id=payload.tenant_id,
+            code=payload.code,
+            name=payload.name,
+            description=payload.description,
+            agent_type=payload.agent_type,
+            owner_user_id=payload.owner_user_id,
+            metadata_json=payload.metadata_json,
+        )
+
+    def list_agents(self, *, tenant_id: str) -> list[AgentDefinition]:
+        return self.agent_repository.list_by_tenant(tenant_id=tenant_id)
+
+    def update_agent_status(
+        self,
+        *,
+        agent_id: str,
+        payload: AgentStatusUpdateRequest,
+    ) -> AgentDefinition | None:
+        return self.agent_repository.update_status(
+            tenant_id=payload.tenant_id,
+            agent_id=agent_id,
+            status=payload.status,
+        )
+
+    def create_agent_version(self, payload: AgentVersionCreateRequest) -> AgentVersion:
+        agent = self.agent_repository.get_by_id(
+            tenant_id=payload.tenant_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(
+            tenant_id=payload.tenant_id,
+            agent_id=payload.agent_id,
+            status=payload.status,
+            role=payload.role,
+            goal=payload.goal,
+            system_prompt=payload.system_prompt,
+            model_config_json=payload.model_config_data.model_dump(mode="json"),
+            memory_policy_json=payload.memory_policy.model_dump(mode="json"),
+            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, *, tenant_id: str, agent_id: str) -> list[AgentVersion]:
+        return self.agent_version_repository.list_by_agent(tenant_id=tenant_id, agent_id=agent_id)
+
+    def create_agent_run(self, payload: AgentRunCreateRequest) -> AgentRun:
+        agent_version = self._resolve_agent_version(
+            tenant_id=payload.tenant_id,
+            agent_id=payload.agent_id,
+            agent_version_id=payload.agent_version_id,
+        )
+        if agent_version is None:
+            raise ValueError("published agent version not found")
+
+        return self.agent_run_repository.create(
+            tenant_id=payload.tenant_id,
+            agent_id=payload.agent_id,
+            agent_version_id=agent_version.id,
+            session_id=payload.session_id,
+            input_text=payload.input_text,
+            input_json=payload.input_json,
+        )
+
+    def list_agent_runs(
+        self,
+        *,
+        tenant_id: str,
+        agent_id: str | None = None,
+        session_id: str | None = None,
+    ) -> list[AgentRun]:
+        return self.agent_run_repository.list_by_scope(
+            tenant_id=tenant_id,
+            agent_id=agent_id,
+            session_id=session_id,
+        )
+
+    def update_agent_run_status(
+        self,
+        *,
+        agent_run_id: str,
+        payload: AgentRunStatusUpdateRequest,
+    ) -> AgentRun | None:
+        entity = self.agent_run_repository.get_by_id(
+            tenant_id=payload.tenant_id,
+            agent_run_id=agent_run_id,
+        )
+        if entity is None:
+            return None
+        return self.agent_run_repository.update_status(
+            agent_run_id=agent_run_id,
+            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,
+        )
+
+    def _resolve_agent_version(
+        self,
+        *,
+        tenant_id: str,
+        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(
+                tenant_id=tenant_id,
+                agent_version_id=agent_version_id,
+            )
+        return self.agent_version_repository.get_latest_published(
+            tenant_id=tenant_id,
+            agent_id=agent_id,
+        )

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

@@ -0,0 +1 @@
+

+ 17 - 0
services/agent-service/app/bootstrap/app.py

@@ -0,0 +1,17 @@
+from fastapi import FastAPI
+
+from app.api.routes import router
+from app.bootstrap.settings import AgentServiceSettings
+from app.db.session import build_session_factory
+
+
+def create_app() -> FastAPI:
+    settings = AgentServiceSettings()
+    app = FastAPI(
+        title="agent-platform agent-service",
+        version="0.1.0",
+    )
+    app.state.settings = settings
+    app.state.session_factory = build_session_factory(settings)
+    app.include_router(router, prefix="/agents", tags=["agents"])
+    return app

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

@@ -0,0 +1,7 @@
+from core_shared import ServiceSettings
+
+
+class AgentServiceSettings(ServiceSettings):
+    service_name: str = "agent-service"
+    service_port: int = 8007
+    database_url: str = "sqlite:///./agent_service.db"

+ 1 - 0
services/agent-service/app/db/__init__.py

@@ -0,0 +1 @@
+

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

@@ -0,0 +1,7 @@
+from core_db import Base
+
+from .agent_definition import AgentDefinition
+from .agent_run import AgentRun
+from .agent_version import AgentVersion
+
+__all__ = ["AgentDefinition", "AgentRun", "AgentVersion", "Base"]

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

@@ -0,0 +1,18 @@
+from sqlalchemy import String, Text
+from sqlalchemy.dialects.sqlite import JSON
+from sqlalchemy.orm import Mapped, mapped_column
+
+from core_db import AuditMixin, Base, TenantMixin, VersionMixin
+from core_shared import JSONValue
+
+
+class AgentDefinition(TenantMixin, AuditMixin, VersionMixin, Base):
+    __tablename__ = "agent_definition"
+
+    code: Mapped[str] = mapped_column(String(64), index=True)
+    name: Mapped[str] = mapped_column(String(128))
+    description: Mapped[str | None] = mapped_column(Text, nullable=True)
+    agent_type: Mapped[str] = mapped_column(String(32), default="assistant")
+    status: Mapped[str] = mapped_column(String(32), default="draft", index=True)
+    owner_user_id: Mapped[str | None] = mapped_column(String(36), nullable=True)
+    metadata_json: Mapped[dict[str, JSONValue] | None] = mapped_column(JSON, nullable=True)

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

@@ -0,0 +1,26 @@
+from datetime import datetime
+
+from sqlalchemy import DateTime, String, Text
+from sqlalchemy.dialects.sqlite import JSON
+from sqlalchemy.orm import Mapped, mapped_column
+
+from core_db import AuditMixin, Base, TenantMixin, VersionMixin
+from core_shared import JSONValue
+
+
+class AgentRun(TenantMixin, AuditMixin, VersionMixin, 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)
+    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)
+    input_text: Mapped[str | None] = mapped_column(Text, nullable=True)
+    input_json: Mapped[dict[str, JSONValue] | None] = mapped_column(JSON, nullable=True)
+    output_text: Mapped[str | None] = mapped_column(Text, nullable=True)
+    output_json: Mapped[dict[str, JSONValue] | None] = mapped_column(JSON, nullable=True)
+    started_time: Mapped[datetime | None] = mapped_column(DateTime, nullable=True)
+    finished_time: Mapped[datetime | None] = mapped_column(DateTime, nullable=True)
+    error_code: Mapped[str | None] = mapped_column(String(64), nullable=True)
+    error_message: Mapped[str | None] = mapped_column(Text, nullable=True)

+ 24 - 0
services/agent-service/app/db/models/agent_version.py

@@ -0,0 +1,24 @@
+from datetime import datetime
+
+from sqlalchemy import DateTime, Integer, String, Text
+from sqlalchemy.dialects.sqlite import JSON
+from sqlalchemy.orm import Mapped, mapped_column
+
+from core_db import AuditMixin, Base, TenantMixin, VersionMixin
+from core_shared import JSONValue
+
+
+class AgentVersion(TenantMixin, AuditMixin, VersionMixin, Base):
+    __tablename__ = "agent_version"
+
+    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)
+    model_config_json: Mapped[dict[str, JSONValue]] = mapped_column(JSON, default=dict)
+    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)

+ 29 - 0
services/agent-service/app/db/session.py

@@ -0,0 +1,29 @@
+from collections.abc import Generator
+
+from fastapi import Request
+from sqlalchemy.orm import Session, sessionmaker
+
+from core_db import DatabaseSettings, create_engine_from_settings, create_session_factory
+
+from app.bootstrap.settings import AgentServiceSettings
+
+
+def build_session_factory(
+    settings: AgentServiceSettings | None = None,
+) -> sessionmaker[Session]:
+    resolved_settings = settings or AgentServiceSettings()
+    db_settings = DatabaseSettings(
+        database_url=resolved_settings.database_url,
+        echo_sql=resolved_settings.echo_sql,
+    )
+    engine = create_engine_from_settings(db_settings)
+    return create_session_factory(engine)
+
+
+def get_db(request: Request) -> Generator[Session, None, None]:
+    session_factory: sessionmaker[Session] = request.app.state.session_factory
+    session = session_factory()
+    try:
+        yield session
+    finally:
+        session.close()

+ 1 - 0
services/agent-service/app/domain/__init__.py

@@ -0,0 +1 @@
+

+ 225 - 0
services/agent-service/app/domain/repositories.py

@@ -0,0 +1,225 @@
+from datetime import datetime
+
+from sqlalchemy import func, select
+from sqlalchemy.orm import Session
+
+from core_domain import AgentRunStatus, AgentStatus, AgentVersionStatus
+from core_shared import JSONValue
+
+from app.db.models import AgentDefinition, AgentRun, AgentVersion
+
+
+class AgentDefinitionRepository:
+    def __init__(self, db: Session) -> None:
+        self.db = db
+
+    def create(
+        self,
+        *,
+        tenant_id: str,
+        code: str,
+        name: str,
+        description: str | None,
+        agent_type: str,
+        owner_user_id: str | None,
+        metadata_json: dict[str, JSONValue] | None,
+    ) -> AgentDefinition:
+        entity = AgentDefinition(
+            tenant_id=tenant_id,
+            code=code,
+            name=name,
+            description=description,
+            agent_type=agent_type,
+            owner_user_id=owner_user_id,
+            metadata_json=metadata_json,
+        )
+        self.db.add(entity)
+        self.db.commit()
+        self.db.refresh(entity)
+        return entity
+
+    def list_by_tenant(self, *, tenant_id: str) -> list[AgentDefinition]:
+        stmt = (
+            select(AgentDefinition)
+            .where(AgentDefinition.tenant_id == tenant_id)
+            .order_by(AgentDefinition.created_time.desc())
+        )
+        return list(self.db.scalars(stmt))
+
+    def get_by_id(self, *, tenant_id: str, agent_id: str) -> AgentDefinition | None:
+        stmt = (
+            select(AgentDefinition)
+            .where(AgentDefinition.tenant_id == tenant_id)
+            .where(AgentDefinition.id == agent_id)
+        )
+        return self.db.scalar(stmt)
+
+    def update_status(
+        self,
+        *,
+        tenant_id: str,
+        agent_id: str,
+        status: AgentStatus,
+    ) -> AgentDefinition | None:
+        entity = self.get_by_id(tenant_id=tenant_id, agent_id=agent_id)
+        if entity is None:
+            return None
+        entity.status = status
+        self.db.commit()
+        self.db.refresh(entity)
+        return entity
+
+
+class AgentVersionRepository:
+    def __init__(self, db: Session) -> None:
+        self.db = db
+
+    def create(
+        self,
+        *,
+        tenant_id: str,
+        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(
+            tenant_id=tenant_id,
+            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,
+        )
+        self.db.add(entity)
+        self.db.commit()
+        self.db.refresh(entity)
+        return entity
+
+    def list_by_agent(self, *, tenant_id: str, agent_id: str) -> list[AgentVersion]:
+        stmt = (
+            select(AgentVersion)
+            .where(AgentVersion.tenant_id == tenant_id)
+            .where(AgentVersion.agent_id == agent_id)
+            .order_by(AgentVersion.version_no.desc())
+        )
+        return list(self.db.scalars(stmt))
+
+    def get_by_id(self, *, tenant_id: str, agent_version_id: str) -> AgentVersion | None:
+        stmt = (
+            select(AgentVersion)
+            .where(AgentVersion.tenant_id == tenant_id)
+            .where(AgentVersion.id == agent_version_id)
+        )
+        return self.db.scalar(stmt)
+
+    def get_latest_published(self, *, tenant_id: str, agent_id: str) -> AgentVersion | None:
+        stmt = (
+            select(AgentVersion)
+            .where(AgentVersion.tenant_id == tenant_id)
+            .where(AgentVersion.agent_id == agent_id)
+            .where(AgentVersion.status == "published")
+            .order_by(AgentVersion.version_no.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
+
+
+class AgentRunRepository:
+    def __init__(self, db: Session) -> None:
+        self.db = db
+
+    def create(
+        self,
+        *,
+        tenant_id: str,
+        agent_id: str,
+        agent_version_id: str,
+        session_id: str | None,
+        input_text: str | None,
+        input_json: dict[str, JSONValue] | None,
+    ) -> AgentRun:
+        entity = AgentRun(
+            tenant_id=tenant_id,
+            agent_id=agent_id,
+            agent_version_id=agent_version_id,
+            session_id=session_id,
+            input_text=input_text,
+            input_json=input_json,
+            status="queued",
+        )
+        self.db.add(entity)
+        self.db.commit()
+        self.db.refresh(entity)
+        return entity
+
+    def list_by_scope(
+        self,
+        *,
+        tenant_id: str,
+        agent_id: str | None = None,
+        session_id: str | None = None,
+    ) -> list[AgentRun]:
+        stmt = select(AgentRun).where(AgentRun.tenant_id == tenant_id)
+        if agent_id is not None:
+            stmt = stmt.where(AgentRun.agent_id == agent_id)
+        if session_id is not None:
+            stmt = stmt.where(AgentRun.session_id == session_id)
+        stmt = stmt.order_by(AgentRun.created_time.desc())
+        return list(self.db.scalars(stmt))
+
+    def get_by_id(self, *, tenant_id: str, agent_run_id: str) -> AgentRun | None:
+        stmt = (
+            select(AgentRun)
+            .where(AgentRun.tenant_id == tenant_id)
+            .where(AgentRun.id == agent_run_id)
+        )
+        return self.db.scalar(stmt)
+
+    def update_status(
+        self,
+        *,
+        agent_run_id: str,
+        status: AgentRunStatus,
+        worker_key: str | None = None,
+        output_text: str | None = None,
+        output_json: dict[str, JSONValue] | None = None,
+        error_code: str | None = None,
+        error_message: str | None = None,
+    ) -> AgentRun | None:
+        entity = self.db.get(AgentRun, agent_run_id)
+        if entity is None:
+            return None
+
+        now = datetime.utcnow()
+        entity.status = status
+        entity.worker_key = worker_key
+        entity.output_text = output_text
+        entity.output_json = output_json
+        entity.error_code = error_code
+        entity.error_message = error_message
+        if status == "running" and entity.started_time is None:
+            entity.started_time = now
+        if status in {"completed", "failed", "cancelled"}:
+            entity.finished_time = now
+
+        self.db.commit()
+        self.db.refresh(entity)
+        return entity

+ 3 - 0
services/agent-service/app/main.py

@@ -0,0 +1,3 @@
+from app.bootstrap.app import create_app
+
+app = create_app()

+ 1 - 0
services/agent-service/app/schemas/__init__.py

@@ -0,0 +1 @@
+

+ 98 - 0
services/agent-service/app/schemas/agent.py

@@ -0,0 +1,98 @@
+from datetime import datetime
+from typing import TYPE_CHECKING
+
+from pydantic import BaseModel, ConfigDict, Field
+
+from core_domain import (
+    AgentDefinitionContract,
+    AgentMemoryPolicyContract,
+    AgentModelConfigContract,
+    AgentRunContract,
+    AgentRunStatus,
+    AgentSkillRefContract,
+    AgentStatus,
+    AgentToolRefContract,
+    AgentVersionContract,
+    AgentVersionStatus,
+)
+from core_shared import JSONValue
+
+if TYPE_CHECKING:
+    from app.db.models import AgentDefinition, AgentRun, AgentVersion
+
+
+class AgentCreateRequest(BaseModel):
+    tenant_id: str
+    code: str
+    name: str
+    description: str | None = None
+    agent_type: str = "assistant"
+    owner_user_id: str | None = None
+    metadata_json: dict[str, JSONValue] = Field(default_factory=dict)
+
+
+class AgentStatusUpdateRequest(BaseModel):
+    tenant_id: str
+    status: AgentStatus
+
+
+class AgentResponse(AgentDefinitionContract):
+    @classmethod
+    def from_entity(cls, entity: "AgentDefinition") -> "AgentResponse":
+        return cls.model_validate(entity, from_attributes=True)
+
+
+class AgentVersionCreateRequest(BaseModel):
+    model_config = ConfigDict(populate_by_name=True)
+
+    tenant_id: str
+    agent_id: str
+    status: AgentVersionStatus = "draft"
+    role: str = "assistant"
+    goal: str | None = None
+    system_prompt: str
+    model_config_data: AgentModelConfigContract = Field(
+        default_factory=AgentModelConfigContract,
+        alias="model_config",
+    )
+    memory_policy: AgentMemoryPolicyContract = Field(default_factory=AgentMemoryPolicyContract)
+    tool_refs: list[AgentToolRefContract] = Field(default_factory=list)
+    skill_refs: list[AgentSkillRefContract] = Field(default_factory=list)
+
+
+class AgentVersionResponse(AgentVersionContract):
+    @classmethod
+    def from_entity(cls, entity: "AgentVersion") -> "AgentVersionResponse":
+        return cls.model_validate(entity, from_attributes=True)
+
+
+class AgentRunCreateRequest(BaseModel):
+    tenant_id: str
+    agent_id: str
+    agent_version_id: str | None = None
+    session_id: str | None = None
+    input_text: str | None = None
+    input_json: dict[str, JSONValue] | None = None
+
+
+class AgentRunStatusUpdateRequest(BaseModel):
+    tenant_id: str
+    status: AgentRunStatus
+    worker_key: str | None = None
+    output_text: str | None = None
+    output_json: dict[str, JSONValue] | None = None
+    error_code: str | None = None
+    error_message: str | None = None
+
+
+class AgentRunResponse(AgentRunContract):
+    @classmethod
+    def from_entity(cls, entity: "AgentRun") -> "AgentRunResponse":
+        return cls.model_validate(entity, from_attributes=True)
+
+
+class AgentHealthResponse(BaseModel):
+    service: str
+    status: str
+    database: str
+    checked_time: datetime

+ 26 - 0
services/agent-service/pyproject.toml

@@ -0,0 +1,26 @@
+[build-system]
+requires = ["setuptools>=68"]
+build-backend = "setuptools.build_meta"
+
+[project]
+name = "agent-service"
+version = "0.1.0"
+description = "Agent definition and run service for agent platform."
+requires-python = ">=3.11"
+dependencies = [
+  "alembic>=1.13,<2.0",
+  "fastapi>=0.111,<1.0",
+  "httpx>=0.27,<1.0",
+  "uvicorn[standard]>=0.30,<1.0",
+  "pydantic>=2.7,<3.0",
+  "sqlalchemy>=2.0,<3.0",
+  "core-db",
+  "core-domain",
+  "core-shared",
+]
+
+[tool.setuptools]
+package-dir = {"" = "."}
+
+[tool.setuptools.packages.find]
+where = ["."]

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

@@ -151,6 +151,12 @@ def build_proxy_targets(settings: ApiGatewaySettings) -> dict[ProxyServiceName,
             path_prefix="/code",
             health_path="/code/health",
         ),
+        "agent-service": ProxyTarget(
+            service_name="agent-service",
+            base_url=settings.agent_service_url,
+            path_prefix="/agents",
+            health_path="/agents/health",
+        ),
     }
 
 
@@ -233,6 +239,27 @@ async def proxy_runtime_service(
     )
 
 
+@router.api_route(
+    "/gateway/agents",
+    methods=["GET", "POST", "PUT", "PATCH", "DELETE"],
+)
+@router.api_route(
+    "/gateway/agents/{path:path}",
+    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:
+    return await proxy.forward(
+        request=request,
+        target=build_proxy_targets(settings)["agent-service"],
+        path=path,
+    )
+
+
 @router.api_route(
     "/gateway/tools",
     methods=["GET", "POST", "PUT", "PATCH", "DELETE"],

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

@@ -11,6 +11,7 @@ class ApiGatewaySettings(ServiceSettings):
     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"
+    agent_service_url: str = "http://127.0.0.1:8007"
     proxy_timeout_seconds: float = 30.0
     downstream_health_timeout_seconds: float = 2.0
     auth_required: bool = False

+ 1 - 0
services/api-gateway/app/infrastructure/proxy.py

@@ -15,6 +15,7 @@ ProxyServiceName = Literal[
     "tool-service",
     "model-gateway-service",
     "code-runner-service",
+    "agent-service",
 ]