Przeglądaj źródła

feat: scaffold multi-service agent platform core

Jax Docker 2 miesięcy temu
rodzic
commit
e6fa6c3ded
100 zmienionych plików z 2514 dodań i 0 usunięć
  1. 16 0
      .gitignore
  2. 179 0
      README.md
  3. 1 0
      deployments/docker/.gitkeep
  4. 1 0
      deployments/k8s/.gitkeep
  5. 19 0
      libs/core-db/pyproject.toml
  6. 13 0
      libs/core-db/src/core_db/__init__.py
  7. 6 0
      libs/core-db/src/core_db/base.py
  8. 27 0
      libs/core-db/src/core_db/mixins.py
  9. 43 0
      libs/core-db/src/core_db/session.py
  10. 19 0
      libs/core-domain/pyproject.toml
  11. 18 0
      libs/core-domain/src/core_domain/__init__.py
  12. 61 0
      libs/core-domain/src/core_domain/runtime_contracts.py
  13. 13 0
      libs/core-domain/src/core_domain/service.py
  14. 19 0
      libs/core-dsl/pyproject.toml
  15. 4 0
      libs/core-dsl/src/core_dsl/__init__.py
  16. 21 0
      libs/core-dsl/src/core_dsl/workflow.py
  17. 19 0
      libs/core-events/pyproject.toml
  18. 4 0
      libs/core-events/src/core_events/__init__.py
  19. 13 0
      libs/core-events/src/core_events/envelope.py
  20. 20 0
      libs/core-shared/pyproject.toml
  21. 4 0
      libs/core-shared/src/core_shared/__init__.py
  22. 18 0
      libs/core-shared/src/core_shared/config.py
  23. 5 0
      libs/core-shared/src/core_shared/types.py
  24. 23 0
      pyproject.toml
  25. 37 0
      services/api-gateway/alembic.ini
  26. 42 0
      services/api-gateway/alembic/env.py
  27. 1 0
      services/api-gateway/alembic/versions/.gitkeep
  28. 1 0
      services/api-gateway/app/__init__.py
  29. 1 0
      services/api-gateway/app/api/__init__.py
  30. 20 0
      services/api-gateway/app/api/routes.py
  31. 1 0
      services/api-gateway/app/bootstrap/__init__.py
  32. 17 0
      services/api-gateway/app/bootstrap/app.py
  33. 8 0
      services/api-gateway/app/bootstrap/settings.py
  34. 1 0
      services/api-gateway/app/db/__init__.py
  35. 4 0
      services/api-gateway/app/db/models/__init__.py
  36. 30 0
      services/api-gateway/app/db/session.py
  37. 4 0
      services/api-gateway/app/main.py
  38. 24 0
      services/api-gateway/pyproject.toml
  39. 37 0
      services/runtime-service/alembic.ini
  40. 42 0
      services/runtime-service/alembic/env.py
  41. 1 0
      services/runtime-service/alembic/versions/.gitkeep
  42. 97 0
      services/runtime-service/alembic/versions/20260422_0001_init_runtime_models.py
  43. 1 0
      services/runtime-service/app/__init__.py
  44. 1 0
      services/runtime-service/app/api/__init__.py
  45. 60 0
      services/runtime-service/app/api/routes.py
  46. 1 0
      services/runtime-service/app/application/__init__.py
  47. 51 0
      services/runtime-service/app/application/services.py
  48. 1 0
      services/runtime-service/app/bootstrap/__init__.py
  49. 17 0
      services/runtime-service/app/bootstrap/app.py
  50. 8 0
      services/runtime-service/app/bootstrap/settings.py
  51. 1 0
      services/runtime-service/app/db/__init__.py
  52. 7 0
      services/runtime-service/app/db/models/__init__.py
  53. 25 0
      services/runtime-service/app/db/models/node_run.py
  54. 28 0
      services/runtime-service/app/db/models/workflow_run.py
  55. 30 0
      services/runtime-service/app/db/session.py
  56. 1 0
      services/runtime-service/app/domain/__init__.py
  57. 102 0
      services/runtime-service/app/domain/repositories.py
  58. 4 0
      services/runtime-service/app/main.py
  59. 1 0
      services/runtime-service/app/schemas/__init__.py
  60. 39 0
      services/runtime-service/app/schemas/run.py
  61. 25 0
      services/runtime-service/pyproject.toml
  62. 37 0
      services/session-service/alembic.ini
  63. 42 0
      services/session-service/alembic/env.py
  64. 1 0
      services/session-service/alembic/versions/.gitkeep
  65. 104 0
      services/session-service/alembic/versions/20260422_0001_init_session_models.py
  66. 1 0
      services/session-service/app/__init__.py
  67. 1 0
      services/session-service/app/api/__init__.py
  68. 108 0
      services/session-service/app/api/routes.py
  69. 1 0
      services/session-service/app/application/__init__.py
  70. 102 0
      services/session-service/app/application/services.py
  71. 1 0
      services/session-service/app/bootstrap/__init__.py
  72. 17 0
      services/session-service/app/bootstrap/app.py
  73. 8 0
      services/session-service/app/bootstrap/settings.py
  74. 1 0
      services/session-service/app/db/__init__.py
  75. 7 0
      services/session-service/app/db/models/__init__.py
  76. 18 0
      services/session-service/app/db/models/message.py
  77. 17 0
      services/session-service/app/db/models/run_request.py
  78. 20 0
      services/session-service/app/db/models/session.py
  79. 30 0
      services/session-service/app/db/session.py
  80. 1 0
      services/session-service/app/domain/__init__.py
  81. 118 0
      services/session-service/app/domain/repositories.py
  82. 1 0
      services/session-service/app/infrastructure/__init__.py
  83. 25 0
      services/session-service/app/infrastructure/runtime_client.py
  84. 4 0
      services/session-service/app/main.py
  85. 1 0
      services/session-service/app/schemas/__init__.py
  86. 34 0
      services/session-service/app/schemas/message.py
  87. 69 0
      services/session-service/app/schemas/run_request.py
  88. 32 0
      services/session-service/app/schemas/session.py
  89. 26 0
      services/session-service/pyproject.toml
  90. 37 0
      services/tool-service/alembic.ini
  91. 42 0
      services/tool-service/alembic/env.py
  92. 1 0
      services/tool-service/alembic/versions/.gitkeep
  93. 97 0
      services/tool-service/alembic/versions/20260422_0001_init_tool_models.py
  94. 1 0
      services/tool-service/app/__init__.py
  95. 1 0
      services/tool-service/app/api/__init__.py
  96. 91 0
      services/tool-service/app/api/routes.py
  97. 1 0
      services/tool-service/app/application/__init__.py
  98. 61 0
      services/tool-service/app/application/services.py
  99. 1 0
      services/tool-service/app/bootstrap/__init__.py
  100. 17 0
      services/tool-service/app/bootstrap/app.py

+ 16 - 0
.gitignore

@@ -0,0 +1,16 @@
+.venv/
+__pycache__/
+*.pyc
+*.pyo
+*.pyd
+.pytest_cache/
+.ruff_cache/
+.mypy_cache/
+dist/
+build/
+.coverage
+htmlcov/
+.idea/
+.vscode/
+.DS_Store
+

+ 179 - 0
README.md

@@ -0,0 +1,179 @@
+# agent-platform
+
+基于 Python 的多服务智能体开发平台脚手架。
+
+当前仓库已经初始化为 Monorepo,包含:
+
+- `services/`:核心微服务
+- `libs/`:共享领域模型、DSL、事件、数据库和公共组件
+- `deployments/`:本地和集群部署占位
+- `docs/`:规划和数据库设计文档
+
+## 当前已创建的服务
+
+- `api-gateway`
+- `session-service`
+- `workflow-service`
+- `runtime-service`
+- `tool-service`
+
+每个服务都提供了最小 `FastAPI` 启动入口和健康检查接口,数据库相关服务也已经带上了 `SQLAlchemy` 模型骨架与 Alembic 目录。
+
+## 当前已创建的共享库
+
+- `core-domain`
+- `core-dsl`
+- `core-events`
+- `core-shared`
+- `core-db`
+
+## 推荐本地开发方式
+
+建议使用 `uv` 或 `pip` 创建虚拟环境后安装各服务依赖。
+
+```powershell
+cd D:\workspace\auto-platform
+python -m venv .venv
+.venv\Scripts\activate
+pip install -e .\libs\core-shared
+pip install -e .\libs\core-domain
+pip install -e .\libs\core-dsl
+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\tool-service
+```
+
+运行示例:
+
+```powershell
+cd D:\workspace\auto-platform\services\api-gateway
+uvicorn app.main:app --reload --port 8000
+```
+
+数据库连接默认使用各服务目录下的 SQLite 文件,也可以通过环境变量覆盖:
+
+```powershell
+$env:AGENT_PLATFORM_DATABASE_URL="postgresql+psycopg://user:password@localhost:5432/workflow_db"
+```
+
+## 数据层脚手架
+
+本轮已经加入:
+
+- `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
+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 '{"tenant_id":"t1","code":"sales_assistant","name":"Sales Assistant"}'
+```
+
+```powershell
+Invoke-RestMethod -Method Post `
+  -Uri http://127.0.0.1:8001/sessions `
+  -ContentType "application/json" `
+  -Body '{"tenant_id":"t1","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 '{"tenant_id":"t1","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 '{"tenant_id":"t1","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 '{"tenant_id":"t1","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"}}'
+```
+
+一条链直接派发到 runtime:
+
+```powershell
+Invoke-RestMethod -Method Post `
+  -Uri http://127.0.0.1:8001/sessions/run-requests/dispatch `
+  -ContentType "application/json" `
+  -Body '{"tenant_id":"t1","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
+Invoke-RestMethod -Method Post `
+  -Uri http://127.0.0.1:8004/tools `
+  -ContentType "application/json" `
+  -Body '{"tenant_id":"t1","code":"search_products","name":"Search Products","tool_type":"http"}'
+```
+
+```powershell
+Invoke-RestMethod -Method Post `
+  -Uri http://127.0.0.1:8004/tools/versions `
+  -ContentType "application/json" `
+  -Body '{"tenant_id":"t1","tool_id":"tool-1","input_schema_json":{"query":{"type":"string"}},"invoke_config_json":{"method":"GET","path":"/products/search"}}'
+```
+
+## 目录结构
+
+```text
+services/
+  api-gateway/
+  session-service/
+  workflow-service/
+  runtime-service/
+  tool-service/
+libs/
+  core-domain/
+  core-dsl/
+  core-events/
+  core-shared/
+  core-db/
+deployments/
+  docker/
+  k8s/
+docs/
+tests/
+```
+
+## 下一步建议
+
+1. 补齐 `V0.1` 的 repository / service 层
+2. 写第一版 Alembic 初始迁移
+3. 接入 PostgreSQL / Redis
+4. 增加 Docker Compose
+5. 开始实现应用、流程、运行三条主链路

+ 1 - 0
deployments/docker/.gitkeep

@@ -0,0 +1 @@
+

+ 1 - 0
deployments/k8s/.gitkeep

@@ -0,0 +1 @@
+

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

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

+ 13 - 0
libs/core-db/src/core_db/__init__.py

@@ -0,0 +1,13 @@
+from .base import Base
+from .mixins import AuditMixin, TenantMixin, VersionMixin
+from .session import DatabaseSettings, create_engine_from_settings, create_session_factory
+
+__all__ = [
+    "AuditMixin",
+    "Base",
+    "DatabaseSettings",
+    "TenantMixin",
+    "VersionMixin",
+    "create_engine_from_settings",
+    "create_session_factory",
+]

+ 6 - 0
libs/core-db/src/core_db/base.py

@@ -0,0 +1,6 @@
+from sqlalchemy.orm import DeclarativeBase
+
+
+class Base(DeclarativeBase):
+    pass
+

+ 27 - 0
libs/core-db/src/core_db/mixins.py

@@ -0,0 +1,27 @@
+from datetime import datetime
+from uuid import uuid4
+
+from sqlalchemy import DateTime, Integer, String
+from sqlalchemy.orm import Mapped, mapped_column
+
+
+class TenantMixin:
+    id: Mapped[str] = mapped_column(String(36), primary_key=True, default=lambda: str(uuid4()))
+    tenant_id: Mapped[str] = mapped_column(String(36), index=True)
+
+
+class AuditMixin:
+    created_by: Mapped[str | None] = mapped_column(String(36), nullable=True)
+    updated_by: Mapped[str | None] = mapped_column(String(36), nullable=True)
+    created_time: Mapped[datetime] = mapped_column(DateTime, default=datetime.utcnow)
+    updated_time: Mapped[datetime] = mapped_column(
+        DateTime,
+        default=datetime.utcnow,
+        onupdate=datetime.utcnow,
+    )
+    deleted_time: Mapped[datetime | None] = mapped_column(DateTime, nullable=True)
+
+
+class VersionMixin:
+    version: Mapped[int] = mapped_column(Integer, default=1)
+

+ 43 - 0
libs/core-db/src/core_db/session.py

@@ -0,0 +1,43 @@
+from collections.abc import Generator
+
+from pydantic import BaseModel, Field
+from sqlalchemy import Engine, create_engine
+from sqlalchemy.orm import Session, sessionmaker
+
+
+class DatabaseSettings(BaseModel):
+    database_url: str = Field(default="sqlite:///./service.db")
+    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,
+    )
+
+
+def create_session_factory(engine: Engine) -> sessionmaker[Session]:
+    return sessionmaker(
+        bind=engine,
+        autoflush=False,
+        autocommit=False,
+        expire_on_commit=False,
+        class_=Session,
+    )
+
+
+def session_scope(session_factory: sessionmaker[Session]) -> Generator[Session, None, None]:
+    session = session_factory()
+    try:
+        yield session
+    finally:
+        session.close()
+

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

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

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

@@ -0,0 +1,18 @@
+from .runtime_contracts import (
+    InitialNodeContract,
+    NodeRunContract,
+    RunBootstrapContract,
+    RunCreateContract,
+    WorkflowRunContract,
+)
+from .service import ServiceDescriptor, ServiceHealth
+
+__all__ = [
+    "InitialNodeContract",
+    "NodeRunContract",
+    "RunBootstrapContract",
+    "RunCreateContract",
+    "ServiceDescriptor",
+    "ServiceHealth",
+    "WorkflowRunContract",
+]

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

@@ -0,0 +1,61 @@
+from datetime import datetime
+
+from pydantic import BaseModel
+
+
+class InitialNodeContract(BaseModel):
+    node_id: str
+    node_type: str
+    status: str = "queued"
+
+
+class RunCreateContract(BaseModel):
+    tenant_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 = "main"
+    trigger_type: str = "user"
+    priority: int = 0
+    initial_node: InitialNodeContract | None = None
+
+
+class WorkflowRunContract(BaseModel):
+    id: str
+    tenant_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: str
+    trigger_type: str
+    priority: int
+    current_node_count: int
+    started_time: datetime | None = None
+    created_time: datetime
+
+
+class NodeRunContract(BaseModel):
+    id: str
+    tenant_id: str
+    run_id: str
+    node_id: str
+    node_type: str
+    attempt_no: int
+    status: str
+    queued_time: datetime | None = None
+    created_time: datetime
+
+
+class RunBootstrapContract(BaseModel):
+    run: WorkflowRunContract
+    initial_node: NodeRunContract | None = None
+

+ 13 - 0
libs/core-domain/src/core_domain/service.py

@@ -0,0 +1,13 @@
+from pydantic import BaseModel
+
+
+class ServiceDescriptor(BaseModel):
+    name: str
+    version: str = "0.1.0"
+    status: str = "ok"
+
+
+class ServiceHealth(BaseModel):
+    service: str
+    status: str = "ok"
+    database: str | None = None

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

@@ -0,0 +1,19 @@
+[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 = [
+  "pydantic>=2.7,<3.0",
+]
+
+[tool.setuptools]
+package-dir = {"" = "src"}
+
+[tool.setuptools.packages.find]
+where = ["src"]
+

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

@@ -0,0 +1,4 @@
+from .workflow import EdgeDefinition, NodeDefinition, WorkflowDefinition
+
+__all__ = ["EdgeDefinition", "NodeDefinition", "WorkflowDefinition"]
+

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

@@ -0,0 +1,21 @@
+from pydantic import BaseModel, Field
+
+
+class NodeDefinition(BaseModel):
+    id: str
+    type: str
+    config: dict = Field(default_factory=dict)
+
+
+class EdgeDefinition(BaseModel):
+    source: str
+    target: str
+    condition: str | None = None
+
+
+class WorkflowDefinition(BaseModel):
+    code: str
+    name: str
+    nodes: list[NodeDefinition] = Field(default_factory=list)
+    edges: list[EdgeDefinition] = Field(default_factory=list)
+

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

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

+ 4 - 0
libs/core-events/src/core_events/__init__.py

@@ -0,0 +1,4 @@
+from .envelope import EventEnvelope
+
+__all__ = ["EventEnvelope"]
+

+ 13 - 0
libs/core-events/src/core_events/envelope.py

@@ -0,0 +1,13 @@
+from datetime import datetime
+from uuid import uuid4
+
+from pydantic import BaseModel, Field
+
+
+class EventEnvelope(BaseModel):
+    event_id: str = Field(default_factory=lambda: str(uuid4()))
+    event_type: str
+    source_service: str
+    payload: dict = Field(default_factory=dict)
+    event_time: datetime = Field(default_factory=datetime.utcnow)
+

+ 20 - 0
libs/core-shared/pyproject.toml

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

+ 4 - 0
libs/core-shared/src/core_shared/__init__.py

@@ -0,0 +1,4 @@
+from .config import ServiceSettings
+from .types import JSONPrimitive, JSONValue
+
+__all__ = ["JSONPrimitive", "JSONValue", "ServiceSettings"]

+ 18 - 0
libs/core-shared/src/core_shared/config.py

@@ -0,0 +1,18 @@
+from pydantic import Field
+from pydantic_settings import BaseSettings, SettingsConfigDict
+
+
+class ServiceSettings(BaseSettings):
+    service_name: str = Field(default="service")
+    service_env: str = Field(default="local")
+    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")
+    echo_sql: bool = Field(default=False)
+
+    model_config = SettingsConfigDict(
+        env_prefix="AGENT_PLATFORM_",
+        env_file=".env",
+        extra="ignore",
+    )

+ 5 - 0
libs/core-shared/src/core_shared/types.py

@@ -0,0 +1,5 @@
+from typing import TypeAlias
+
+JSONPrimitive: TypeAlias = str | int | float | bool | None
+JSONValue: TypeAlias = JSONPrimitive | dict[str, "JSONValue"] | list["JSONValue"]
+

+ 23 - 0
pyproject.toml

@@ -0,0 +1,23 @@
+[tool.uv.workspace]
+members = [
+  "libs/core-db",
+  "libs/core-domain",
+  "libs/core-dsl",
+  "libs/core-events",
+  "libs/core-shared",
+  "services/api-gateway",
+  "services/session-service",
+  "services/workflow-service",
+  "services/runtime-service",
+  "services/tool-service",
+]
+
+[tool.ruff]
+line-length = 100
+target-version = "py311"
+
+[tool.ruff.lint]
+select = ["E", "F", "I", "UP", "B"]
+
+[tool.pytest.ini_options]
+testpaths = ["tests"]

+ 37 - 0
services/api-gateway/alembic.ini

@@ -0,0 +1,37 @@
+[alembic]
+script_location = alembic
+prepend_sys_path = .
+sqlalchemy.url = sqlite:///./api_gateway.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
+

+ 42 - 0
services/api-gateway/alembic/env.py

@@ -0,0 +1,42 @@
+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/api-gateway/alembic/versions/.gitkeep

@@ -0,0 +1 @@
+

+ 1 - 0
services/api-gateway/app/__init__.py

@@ -0,0 +1 @@
+

+ 1 - 0
services/api-gateway/app/api/__init__.py

@@ -0,0 +1 @@
+

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

@@ -0,0 +1,20 @@
+from fastapi import APIRouter, Depends
+from sqlalchemy import text
+from sqlalchemy.orm import Session
+
+from core_domain import ServiceDescriptor, ServiceHealth
+from app.db.session import get_db
+
+router = APIRouter()
+
+
+@router.get("/health", response_model=ServiceDescriptor)
+def health_check(db: Session = Depends(get_db)) -> 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:
+    db.execute(text("SELECT 1"))
+    return ServiceHealth(service="api-gateway", status="ok", database="ok")

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

@@ -0,0 +1 @@
+

+ 17 - 0
services/api-gateway/app/bootstrap/app.py

@@ -0,0 +1,17 @@
+from fastapi import FastAPI
+
+from app.api.routes import router
+from app.bootstrap.settings import ApiGatewaySettings
+from app.db.session import build_session_factory
+
+
+def create_app() -> FastAPI:
+    settings = ApiGatewaySettings()
+    app = FastAPI(
+        title="agent-platform api-gateway",
+        version="0.1.0",
+    )
+    app.state.settings = settings
+    app.state.session_factory = build_session_factory(settings)
+    app.include_router(router)
+    return app

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

@@ -0,0 +1,8 @@
+from core_shared import ServiceSettings
+
+
+class ApiGatewaySettings(ServiceSettings):
+    service_name: str = "api-gateway"
+    service_port: int = 8000
+    database_url: str = "sqlite:///./api_gateway.db"
+

+ 1 - 0
services/api-gateway/app/db/__init__.py

@@ -0,0 +1 @@
+

+ 4 - 0
services/api-gateway/app/db/models/__init__.py

@@ -0,0 +1,4 @@
+from core_db import Base
+
+__all__ = ["Base"]
+

+ 30 - 0
services/api-gateway/app/db/session.py

@@ -0,0 +1,30 @@
+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 ApiGatewaySettings
+
+
+def build_session_factory(
+    settings: ApiGatewaySettings | None = None,
+) -> sessionmaker[Session]:
+    resolved_settings = settings or ApiGatewaySettings()
+    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()
+

+ 4 - 0
services/api-gateway/app/main.py

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

+ 24 - 0
services/api-gateway/pyproject.toml

@@ -0,0 +1,24 @@
+[build-system]
+requires = ["setuptools>=68"]
+build-backend = "setuptools.build_meta"
+
+[project]
+name = "api-gateway"
+version = "0.1.0"
+description = "API gateway for agent platform."
+requires-python = ">=3.11"
+dependencies = [
+  "alembic>=1.13,<2.0",
+  "fastapi>=0.111,<1.0",
+  "sqlalchemy>=2.0,<3.0",
+  "uvicorn[standard]>=0.30,<1.0",
+  "core-db",
+  "core-domain",
+  "core-shared",
+]
+
+[tool.setuptools]
+package-dir = {"" = "."}
+
+[tool.setuptools.packages.find]
+where = ["."]

+ 37 - 0
services/runtime-service/alembic.ini

@@ -0,0 +1,37 @@
+[alembic]
+script_location = alembic
+prepend_sys_path = .
+sqlalchemy.url = sqlite:///./runtime_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
+

+ 42 - 0
services/runtime-service/alembic/env.py

@@ -0,0 +1,42 @@
+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/runtime-service/alembic/versions/.gitkeep

@@ -0,0 +1 @@
+

+ 97 - 0
services/runtime-service/alembic/versions/20260422_0001_init_runtime_models.py

@@ -0,0 +1,97 @@
+"""init runtime models
+
+Revision ID: 20260422_0001
+Revises:
+Create Date: 2026-04-22 17:20:00
+"""
+
+from collections.abc import Sequence
+
+from alembic import op
+import sqlalchemy as sa
+
+
+revision: str = "20260422_0001"
+down_revision: str | None = None
+branch_labels: Sequence[str] | None = None
+depends_on: Sequence[str] | None = None
+
+
+def upgrade() -> None:
+    op.create_table(
+        "workflow_run",
+        sa.Column("app_id", sa.String(length=36), nullable=False),
+        sa.Column("app_version_id", sa.String(length=36), nullable=False),
+        sa.Column("workflow_id", sa.String(length=36), nullable=False),
+        sa.Column("workflow_version_id", sa.String(length=36), nullable=False),
+        sa.Column("session_id", sa.String(length=36), nullable=True),
+        sa.Column("parent_run_id", sa.String(length=36), nullable=True),
+        sa.Column("root_run_id", sa.String(length=36), nullable=True),
+        sa.Column("run_type", sa.String(length=32), nullable=False),
+        sa.Column("status", sa.String(length=32), nullable=False),
+        sa.Column("trigger_type", sa.String(length=32), nullable=False),
+        sa.Column("priority", sa.Integer(), nullable=False),
+        sa.Column("current_node_count", sa.Integer(), nullable=False),
+        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_workflow_run_app_id", "workflow_run", ["app_id"], unique=False)
+    op.create_index("ix_workflow_run_root_run_id", "workflow_run", ["root_run_id"], unique=False)
+    op.create_index("ix_workflow_run_session_id", "workflow_run", ["session_id"], unique=False)
+    op.create_index("ix_workflow_run_status", "workflow_run", ["status"], unique=False)
+    op.create_index("ix_workflow_run_tenant_id", "workflow_run", ["tenant_id"], unique=False)
+
+    op.create_table(
+        "node_run",
+        sa.Column("run_id", sa.String(length=36), nullable=False),
+        sa.Column("parent_node_run_id", sa.String(length=36), nullable=True),
+        sa.Column("node_id", sa.String(length=128), nullable=False),
+        sa.Column("node_type", sa.String(length=32), nullable=False),
+        sa.Column("attempt_no", sa.Integer(), nullable=False),
+        sa.Column("status", sa.String(length=32), nullable=False),
+        sa.Column("worker_key", sa.String(length=128), nullable=True),
+        sa.Column("lease_expire_time", sa.DateTime(), nullable=True),
+        sa.Column("queued_time", sa.DateTime(), 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_node_run_run_id", "node_run", ["run_id"], unique=False)
+    op.create_index("ix_node_run_status", "node_run", ["status"], unique=False)
+    op.create_index("ix_node_run_tenant_id", "node_run", ["tenant_id"], unique=False)
+
+
+def downgrade() -> None:
+    op.drop_index("ix_node_run_tenant_id", table_name="node_run")
+    op.drop_index("ix_node_run_status", table_name="node_run")
+    op.drop_index("ix_node_run_run_id", table_name="node_run")
+    op.drop_table("node_run")
+
+    op.drop_index("ix_workflow_run_tenant_id", table_name="workflow_run")
+    op.drop_index("ix_workflow_run_status", table_name="workflow_run")
+    op.drop_index("ix_workflow_run_session_id", table_name="workflow_run")
+    op.drop_index("ix_workflow_run_root_run_id", table_name="workflow_run")
+    op.drop_index("ix_workflow_run_app_id", table_name="workflow_run")
+    op.drop_table("workflow_run")
+

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

@@ -0,0 +1 @@
+

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

@@ -0,0 +1 @@
+

+ 60 - 0
services/runtime-service/app/api/routes.py

@@ -0,0 +1,60 @@
+from fastapi import APIRouter, Depends, Query
+from sqlalchemy import text
+from sqlalchemy.orm import Session
+
+from core_domain import ServiceHealth
+from app.application.services import RuntimeApplicationService
+from app.db.session import get_db
+from app.domain.repositories import NodeRunRepository, WorkflowRunRepository
+from app.schemas.run import RunBootstrapResponse, RunCreateRequest, NodeRunResponse, WorkflowRunResponse
+
+router = APIRouter()
+
+
+def get_runtime_application_service(db: Session = Depends(get_db)) -> RuntimeApplicationService:
+    return RuntimeApplicationService(
+        workflow_run_repository=WorkflowRunRepository(db),
+        node_run_repository=NodeRunRepository(db),
+    )
+
+
+@router.get("/health", response_model=ServiceHealth)
+def health_check(db: Session = Depends(get_db)) -> ServiceHealth:
+    db.execute(text("SELECT 1"))
+    return ServiceHealth(service="runtime-service", status="ok", database="ok")
+
+
+@router.post("/runs", response_model=RunBootstrapResponse)
+def create_run(
+    payload: RunCreateRequest,
+    service: RuntimeApplicationService = Depends(get_runtime_application_service),
+) -> RunBootstrapResponse:
+    workflow_run, initial_node = service.create_run(payload)
+    return RunBootstrapResponse(
+        run=WorkflowRunResponse.from_entity(workflow_run),
+        initial_node=NodeRunResponse.from_entity(initial_node) if initial_node else None,
+    )
+
+
+@router.get("/runs", response_model=list[WorkflowRunResponse])
+def list_runs(
+    tenant_id: str = Query(...),
+    session_id: str | None = Query(default=None),
+    service: RuntimeApplicationService = Depends(get_runtime_application_service),
+) -> list[WorkflowRunResponse]:
+    return [
+        WorkflowRunResponse.from_entity(item)
+        for item in service.list_runs(tenant_id=tenant_id, session_id=session_id)
+    ]
+
+
+@router.get("/node-runs", response_model=list[NodeRunResponse])
+def list_node_runs(
+    tenant_id: str = Query(...),
+    run_id: str = Query(...),
+    service: RuntimeApplicationService = Depends(get_runtime_application_service),
+) -> list[NodeRunResponse]:
+    return [
+        NodeRunResponse.from_entity(item)
+        for item in service.list_node_runs(tenant_id=tenant_id, run_id=run_id)
+    ]

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

@@ -0,0 +1 @@
+

+ 51 - 0
services/runtime-service/app/application/services.py

@@ -0,0 +1,51 @@
+from app.db.models import NodeRun, WorkflowRun
+from app.domain.repositories import NodeRunRepository, WorkflowRunRepository
+from app.schemas.run import RunCreateRequest
+
+
+class RuntimeApplicationService:
+    def __init__(
+        self,
+        workflow_run_repository: WorkflowRunRepository,
+        node_run_repository: NodeRunRepository,
+    ) -> None:
+        self.workflow_run_repository = workflow_run_repository
+        self.node_run_repository = node_run_repository
+
+    def create_run(self, payload: RunCreateRequest) -> tuple[WorkflowRun, NodeRun | None]:
+        workflow_run = self.workflow_run_repository.create(
+            tenant_id=payload.tenant_id,
+            app_id=payload.app_id,
+            app_version_id=payload.app_version_id,
+            workflow_id=payload.workflow_id,
+            workflow_version_id=payload.workflow_version_id,
+            session_id=payload.session_id,
+            parent_run_id=payload.parent_run_id,
+            root_run_id=payload.root_run_id,
+            run_type=payload.run_type,
+            trigger_type=payload.trigger_type,
+            priority=payload.priority,
+        )
+
+        initial_node = None
+        if payload.initial_node is not None:
+            self.workflow_run_repository.update_node_count(
+                run_id=workflow_run.id,
+                current_node_count=1,
+            )
+            initial_node = self.node_run_repository.create(
+                tenant_id=payload.tenant_id,
+                run_id=workflow_run.id,
+                node_id=payload.initial_node.node_id,
+                node_type=payload.initial_node.node_type,
+                status=payload.initial_node.status,
+            )
+
+        return workflow_run, initial_node
+
+    def list_runs(self, tenant_id: str, session_id: str | None = None) -> list[WorkflowRun]:
+        return self.workflow_run_repository.list_by_scope(tenant_id=tenant_id, session_id=session_id)
+
+    def list_node_runs(self, tenant_id: str, run_id: str) -> list[NodeRun]:
+        return self.node_run_repository.list_by_run(tenant_id=tenant_id, run_id=run_id)
+

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

@@ -0,0 +1 @@
+

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

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

+ 8 - 0
services/runtime-service/app/bootstrap/settings.py

@@ -0,0 +1,8 @@
+from core_shared import ServiceSettings
+
+
+class RuntimeServiceSettings(ServiceSettings):
+    service_name: str = "runtime-service"
+    service_port: int = 8003
+    database_url: str = "sqlite:///./runtime_service.db"
+

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

@@ -0,0 +1 @@
+

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

@@ -0,0 +1,7 @@
+from core_db import Base
+
+from .node_run import NodeRun
+from .workflow_run import WorkflowRun
+
+__all__ = ["Base", "NodeRun", "WorkflowRun"]
+

+ 25 - 0
services/runtime-service/app/db/models/node_run.py

@@ -0,0 +1,25 @@
+from datetime import datetime
+
+from sqlalchemy import DateTime, Integer, String, Text
+from sqlalchemy.orm import Mapped, mapped_column
+
+from core_db import AuditMixin, Base, TenantMixin, VersionMixin
+
+
+class NodeRun(TenantMixin, AuditMixin, VersionMixin, Base):
+    __tablename__ = "node_run"
+
+    run_id: Mapped[str] = mapped_column(String(36), index=True)
+    parent_node_run_id: Mapped[str | None] = mapped_column(String(36), nullable=True)
+    node_id: Mapped[str] = mapped_column(String(128))
+    node_type: Mapped[str] = mapped_column(String(32))
+    attempt_no: Mapped[int] = mapped_column(Integer, default=1)
+    status: Mapped[str] = mapped_column(String(32), default="pending", index=True)
+    worker_key: Mapped[str | None] = mapped_column(String(128), nullable=True)
+    lease_expire_time: Mapped[datetime | None] = mapped_column(DateTime, nullable=True)
+    queued_time: Mapped[datetime | None] = mapped_column(DateTime, 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)
+

+ 28 - 0
services/runtime-service/app/db/models/workflow_run.py

@@ -0,0 +1,28 @@
+from datetime import datetime
+
+from sqlalchemy import DateTime, Integer, String, Text
+from sqlalchemy.orm import Mapped, mapped_column
+
+from core_db import AuditMixin, Base, TenantMixin, VersionMixin
+
+
+class WorkflowRun(TenantMixin, AuditMixin, VersionMixin, Base):
+    __tablename__ = "workflow_run"
+
+    app_id: Mapped[str] = mapped_column(String(36), index=True)
+    app_version_id: Mapped[str] = mapped_column(String(36))
+    workflow_id: Mapped[str] = mapped_column(String(36))
+    workflow_version_id: Mapped[str] = mapped_column(String(36))
+    session_id: Mapped[str | None] = mapped_column(String(36), nullable=True, index=True)
+    parent_run_id: Mapped[str | None] = mapped_column(String(36), nullable=True)
+    root_run_id: Mapped[str | None] = mapped_column(String(36), nullable=True, index=True)
+    run_type: Mapped[str] = mapped_column(String(32), default="main")
+    status: Mapped[str] = mapped_column(String(32), default="pending", index=True)
+    trigger_type: Mapped[str] = mapped_column(String(32), default="user")
+    priority: Mapped[int] = mapped_column(Integer, default=0)
+    current_node_count: Mapped[int] = mapped_column(Integer, default=0)
+    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)
+

+ 30 - 0
services/runtime-service/app/db/session.py

@@ -0,0 +1,30 @@
+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 RuntimeServiceSettings
+
+
+def build_session_factory(
+    settings: RuntimeServiceSettings | None = None,
+) -> sessionmaker[Session]:
+    resolved_settings = settings or RuntimeServiceSettings()
+    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/runtime-service/app/domain/__init__.py

@@ -0,0 +1 @@
+

+ 102 - 0
services/runtime-service/app/domain/repositories.py

@@ -0,0 +1,102 @@
+from datetime import datetime
+
+from sqlalchemy import select
+from sqlalchemy.orm import Session
+
+from app.db.models import NodeRun, WorkflowRun
+
+
+class WorkflowRunRepository:
+    def __init__(self, db: Session) -> None:
+        self.db = db
+
+    def create(
+        self,
+        *,
+        tenant_id: str,
+        app_id: str,
+        app_version_id: str,
+        workflow_id: str,
+        workflow_version_id: str,
+        session_id: str | None,
+        parent_run_id: str | None,
+        root_run_id: str | None,
+        run_type: str,
+        trigger_type: str,
+        priority: int,
+    ) -> WorkflowRun:
+        now = datetime.utcnow()
+        entity = WorkflowRun(
+            tenant_id=tenant_id,
+            app_id=app_id,
+            app_version_id=app_version_id,
+            workflow_id=workflow_id,
+            workflow_version_id=workflow_version_id,
+            session_id=session_id,
+            parent_run_id=parent_run_id,
+            root_run_id=root_run_id,
+            run_type=run_type,
+            trigger_type=trigger_type,
+            priority=priority,
+            status="running",
+            started_time=now,
+        )
+        self.db.add(entity)
+        self.db.commit()
+        if entity.root_run_id is None:
+            entity.root_run_id = entity.id
+            self.db.commit()
+        self.db.refresh(entity)
+        return entity
+
+    def list_by_scope(self, *, tenant_id: str, session_id: str | None = None) -> list[WorkflowRun]:
+        stmt = select(WorkflowRun).where(WorkflowRun.tenant_id == tenant_id)
+        if session_id:
+            stmt = stmt.where(WorkflowRun.session_id == session_id)
+        stmt = stmt.order_by(WorkflowRun.created_time.desc())
+        return list(self.db.scalars(stmt))
+
+    def update_node_count(self, *, run_id: str, current_node_count: int) -> None:
+        entity = self.db.get(WorkflowRun, run_id)
+        if entity is None:
+            return
+        entity.current_node_count = current_node_count
+        self.db.commit()
+
+
+class NodeRunRepository:
+    def __init__(self, db: Session) -> None:
+        self.db = db
+
+    def create(
+        self,
+        *,
+        tenant_id: str,
+        run_id: str,
+        node_id: str,
+        node_type: str,
+        status: str,
+    ) -> NodeRun:
+        now = datetime.utcnow()
+        entity = NodeRun(
+            tenant_id=tenant_id,
+            run_id=run_id,
+            node_id=node_id,
+            node_type=node_type,
+            status=status,
+            queued_time=now,
+        )
+        self.db.add(entity)
+        self.db.commit()
+        self.db.refresh(entity)
+        return entity
+
+    def list_by_run(self, *, tenant_id: str, run_id: str) -> list[NodeRun]:
+        stmt = (
+            select(NodeRun)
+            .where(NodeRun.tenant_id == tenant_id)
+            .where(NodeRun.run_id == run_id)
+            .order_by(NodeRun.created_time.asc())
+        )
+        return list(self.db.scalars(stmt))
+

+ 4 - 0
services/runtime-service/app/main.py

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

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

@@ -0,0 +1 @@
+

+ 39 - 0
services/runtime-service/app/schemas/run.py

@@ -0,0 +1,39 @@
+from typing import TYPE_CHECKING
+
+from core_domain import (
+    InitialNodeContract,
+    NodeRunContract,
+    RunBootstrapContract,
+    RunCreateContract,
+    WorkflowRunContract,
+)
+
+if TYPE_CHECKING:
+    from app.db.models import NodeRun, WorkflowRun
+
+
+class InitialNodeCreateRequest(InitialNodeContract):
+    pass
+
+
+class RunCreateRequest(RunCreateContract):
+    initial_node: InitialNodeCreateRequest | None = None
+
+
+class WorkflowRunResponse(WorkflowRunContract):
+
+    @classmethod
+    def from_entity(cls, entity: "WorkflowRun") -> "WorkflowRunResponse":
+        return cls.model_validate(entity, from_attributes=True)
+
+
+class NodeRunResponse(NodeRunContract):
+
+    @classmethod
+    def from_entity(cls, entity: "NodeRun") -> "NodeRunResponse":
+        return cls.model_validate(entity, from_attributes=True)
+
+
+class RunBootstrapResponse(RunBootstrapContract):
+    run: WorkflowRunResponse
+    initial_node: NodeRunResponse | None = None

+ 25 - 0
services/runtime-service/pyproject.toml

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

+ 37 - 0
services/session-service/alembic.ini

@@ -0,0 +1,37 @@
+[alembic]
+script_location = alembic
+prepend_sys_path = .
+sqlalchemy.url = sqlite:///./session_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
+

+ 42 - 0
services/session-service/alembic/env.py

@@ -0,0 +1,42 @@
+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/session-service/alembic/versions/.gitkeep

@@ -0,0 +1 @@
+

+ 104 - 0
services/session-service/alembic/versions/20260422_0001_init_session_models.py

@@ -0,0 +1,104 @@
+"""init session models
+
+Revision ID: 20260422_0001
+Revises:
+Create Date: 2026-04-22 17:20:00
+"""
+
+from collections.abc import Sequence
+
+from alembic import op
+import sqlalchemy as sa
+
+
+revision: str = "20260422_0001"
+down_revision: str | None = None
+branch_labels: Sequence[str] | None = None
+depends_on: Sequence[str] | None = None
+
+
+def upgrade() -> None:
+    op.create_table(
+        "session",
+        sa.Column("app_id", sa.String(length=36), nullable=False),
+        sa.Column("user_id", sa.String(length=36), nullable=False),
+        sa.Column("channel_type", sa.String(length=32), nullable=False),
+        sa.Column("session_status", sa.String(length=32), nullable=False),
+        sa.Column("title", sa.String(length=256), nullable=True),
+        sa.Column("started_time", sa.DateTime(), nullable=True),
+        sa.Column("last_active_time", sa.DateTime(), nullable=True),
+        sa.Column("closed_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_session_app_id", "session", ["app_id"], unique=False)
+    op.create_index("ix_session_tenant_id", "session", ["tenant_id"], unique=False)
+    op.create_index("ix_session_user_id", "session", ["user_id"], unique=False)
+
+    op.create_table(
+        "message",
+        sa.Column("session_id", sa.String(length=36), nullable=False),
+        sa.Column("turn_id", sa.String(length=36), nullable=True),
+        sa.Column("role", sa.String(length=32), nullable=False),
+        sa.Column("content_type", sa.String(length=32), nullable=False),
+        sa.Column("content_text", sa.Text(), nullable=True),
+        sa.Column("content_json", sa.JSON(), nullable=True),
+        sa.Column("token_count", sa.Integer(), 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_message_session_id", "message", ["session_id"], unique=False)
+    op.create_index("ix_message_tenant_id", "message", ["tenant_id"], unique=False)
+    op.create_index("ix_message_turn_id", "message", ["turn_id"], unique=False)
+
+    op.create_table(
+        "run_request",
+        sa.Column("session_id", sa.String(length=36), nullable=False),
+        sa.Column("app_version_id", sa.String(length=36), nullable=False),
+        sa.Column("workflow_version_id", sa.String(length=36), nullable=False),
+        sa.Column("trigger_type", sa.String(length=32), nullable=False),
+        sa.Column("request_payload_json", sa.JSON(), nullable=True),
+        sa.Column("request_status", sa.String(length=32), nullable=False),
+        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_run_request_session_id", "run_request", ["session_id"], unique=False)
+    op.create_index("ix_run_request_tenant_id", "run_request", ["tenant_id"], unique=False)
+
+
+def downgrade() -> None:
+    op.drop_index("ix_run_request_tenant_id", table_name="run_request")
+    op.drop_index("ix_run_request_session_id", table_name="run_request")
+    op.drop_table("run_request")
+
+    op.drop_index("ix_message_turn_id", table_name="message")
+    op.drop_index("ix_message_tenant_id", table_name="message")
+    op.drop_index("ix_message_session_id", table_name="message")
+    op.drop_table("message")
+
+    op.drop_index("ix_session_user_id", table_name="session")
+    op.drop_index("ix_session_tenant_id", table_name="session")
+    op.drop_index("ix_session_app_id", table_name="session")
+    op.drop_table("session")
+

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

@@ -0,0 +1 @@
+

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

@@ -0,0 +1 @@
+

+ 108 - 0
services/session-service/app/api/routes.py

@@ -0,0 +1,108 @@
+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 SessionApplicationService
+from app.bootstrap.settings import SessionServiceSettings
+from app.db.session import get_db
+from app.domain.repositories import MessageRepository, RunRequestRepository, SessionRepository
+from app.infrastructure.runtime_client import RuntimeServiceClient, RuntimeServiceClientError
+from app.schemas.message import MessageCreateRequest, MessageResponse
+from app.schemas.run_request import DispatchRunRequest, DispatchRunResponse, RunRequestCreateRequest, RunRequestResponse
+from app.schemas.session import SessionCreateRequest, SessionResponse
+
+router = APIRouter()
+
+
+def get_session_settings() -> SessionServiceSettings:
+    return SessionServiceSettings()
+
+
+def get_session_application_service(
+    db: Session = Depends(get_db),
+    settings: SessionServiceSettings = Depends(get_session_settings),
+) -> SessionApplicationService:
+    return SessionApplicationService(
+        session_repository=SessionRepository(db),
+        message_repository=MessageRepository(db),
+        run_request_repository=RunRequestRepository(db),
+        runtime_client=RuntimeServiceClient(base_url=settings.runtime_service_url),
+    )
+
+
+@router.get("/health", response_model=ServiceHealth)
+def health_check(db: Session = Depends(get_db)) -> ServiceHealth:
+    db.execute(text("SELECT 1"))
+    return ServiceHealth(service="session-service", status="ok", database="ok")
+
+
+@router.post("", response_model=SessionResponse)
+def create_session(
+    payload: SessionCreateRequest,
+    service: SessionApplicationService = Depends(get_session_application_service),
+) -> SessionResponse:
+    entity = service.create_session(payload)
+    return SessionResponse.from_entity(entity)
+
+
+@router.get("", response_model=list[SessionResponse])
+def list_sessions(
+    tenant_id: str = Query(...),
+    app_id: str | None = Query(default=None),
+    service: SessionApplicationService = Depends(get_session_application_service),
+) -> list[SessionResponse]:
+    return [SessionResponse.from_entity(item) for item in service.list_sessions(tenant_id, app_id)]
+
+
+@router.post("/messages", response_model=MessageResponse)
+def create_message(
+    payload: MessageCreateRequest,
+    service: SessionApplicationService = Depends(get_session_application_service),
+) -> MessageResponse:
+    entity = service.create_message(payload)
+    return MessageResponse.from_entity(entity)
+
+
+@router.get("/messages", response_model=list[MessageResponse])
+def list_messages(
+    tenant_id: str = Query(...),
+    session_id: str = Query(...),
+    service: SessionApplicationService = Depends(get_session_application_service),
+) -> list[MessageResponse]:
+    return [
+        MessageResponse.from_entity(item)
+        for item in service.list_messages(tenant_id=tenant_id, session_id=session_id)
+    ]
+
+
+@router.post("/run-requests", response_model=RunRequestResponse)
+def create_run_request(
+    payload: RunRequestCreateRequest,
+    service: SessionApplicationService = Depends(get_session_application_service),
+) -> RunRequestResponse:
+    entity = service.create_run_request(payload)
+    return RunRequestResponse.from_entity(entity)
+
+
+@router.get("/run-requests", response_model=list[RunRequestResponse])
+def list_run_requests(
+    tenant_id: str = Query(...),
+    session_id: str = Query(...),
+    service: SessionApplicationService = Depends(get_session_application_service),
+) -> list[RunRequestResponse]:
+    return [
+        RunRequestResponse.from_entity(item)
+        for item in service.list_run_requests(tenant_id=tenant_id, session_id=session_id)
+    ]
+
+
+@router.post("/run-requests/dispatch", response_model=DispatchRunResponse)
+def dispatch_run_request(
+    payload: DispatchRunRequest,
+    service: SessionApplicationService = Depends(get_session_application_service),
+) -> DispatchRunResponse:
+    try:
+        return service.dispatch_run_request(payload)
+    except RuntimeServiceClientError as exc:
+        raise HTTPException(status_code=502, detail=str(exc)) from exc

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

@@ -0,0 +1 @@
+

+ 102 - 0
services/session-service/app/application/services.py

@@ -0,0 +1,102 @@
+from core_domain import InitialNodeContract, RunCreateContract
+
+from app.db.models import Message, RunRequest, Session as SessionModel
+from app.domain.repositories import MessageRepository, RunRequestRepository, SessionRepository
+from app.infrastructure.runtime_client import RuntimeServiceClient
+from app.schemas.message import MessageCreateRequest
+from app.schemas.run_request import DispatchRunRequest, DispatchRunResponse, RunRequestCreateRequest
+from app.schemas.session import SessionCreateRequest
+
+
+class SessionApplicationService:
+    def __init__(
+        self,
+        session_repository: SessionRepository,
+        message_repository: MessageRepository,
+        run_request_repository: RunRequestRepository,
+        runtime_client: RuntimeServiceClient | None = None,
+    ) -> None:
+        self.session_repository = session_repository
+        self.message_repository = message_repository
+        self.run_request_repository = run_request_repository
+        self.runtime_client = runtime_client
+
+    def create_session(self, payload: SessionCreateRequest) -> SessionModel:
+        return self.session_repository.create(
+            tenant_id=payload.tenant_id,
+            app_id=payload.app_id,
+            user_id=payload.user_id,
+            channel_type=payload.channel_type,
+            title=payload.title,
+        )
+
+    def list_sessions(self, tenant_id: str, app_id: str | None = None) -> list[SessionModel]:
+        return self.session_repository.list_by_scope(tenant_id=tenant_id, app_id=app_id)
+
+    def create_message(self, payload: MessageCreateRequest) -> Message:
+        return self.message_repository.create(
+            tenant_id=payload.tenant_id,
+            session_id=payload.session_id,
+            turn_id=payload.turn_id,
+            role=payload.role,
+            content_type=payload.content_type,
+            content_text=payload.content_text,
+            content_json=payload.content_json,
+        )
+
+    def list_messages(self, tenant_id: str, session_id: str) -> list[Message]:
+        return self.message_repository.list_by_session(tenant_id=tenant_id, session_id=session_id)
+
+    def create_run_request(self, payload: RunRequestCreateRequest) -> RunRequest:
+        return self.run_request_repository.create(
+            tenant_id=payload.tenant_id,
+            session_id=payload.session_id,
+            app_version_id=payload.app_version_id,
+            workflow_version_id=payload.workflow_version_id,
+            trigger_type=payload.trigger_type,
+            request_payload_json=payload.request_payload_json,
+            request_status=payload.request_status,
+        )
+
+    def list_run_requests(self, tenant_id: str, session_id: str) -> list[RunRequest]:
+        return self.run_request_repository.list_by_session(tenant_id=tenant_id, session_id=session_id)
+
+    def dispatch_run_request(self, payload: DispatchRunRequest) -> DispatchRunResponse:
+        run_request = self.create_run_request(
+            RunRequestCreateRequest(
+                tenant_id=payload.tenant_id,
+                session_id=payload.session_id,
+                app_version_id=payload.app_version_id,
+                workflow_version_id=payload.workflow_version_id,
+                trigger_type=payload.trigger_type,
+                request_payload_json=payload.request_payload_json,
+                request_status="accepted",
+            )
+        )
+
+        if self.runtime_client is None:
+            raise RuntimeError("runtime client is not configured")
+
+        runtime_response = self.runtime_client.create_run(
+            RunCreateContract(
+                tenant_id=payload.tenant_id,
+                app_id=payload.app_id,
+                app_version_id=payload.app_version_id,
+                workflow_id=payload.workflow_id,
+                workflow_version_id=payload.workflow_version_id,
+                session_id=payload.session_id,
+                trigger_type=payload.trigger_type,
+                priority=payload.priority,
+                initial_node=(
+                    InitialNodeContract(
+                        node_id=payload.initial_node.node_id,
+                        node_type=payload.initial_node.node_type,
+                        status=payload.initial_node.status,
+                    )
+                    if payload.initial_node is not None
+                    else None
+                ),
+            )
+        )
+
+        return DispatchRunResponse.from_parts(run_request=run_request, runtime_response=runtime_response)

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

@@ -0,0 +1 @@
+

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

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

+ 8 - 0
services/session-service/app/bootstrap/settings.py

@@ -0,0 +1,8 @@
+from core_shared import ServiceSettings
+
+
+class SessionServiceSettings(ServiceSettings):
+    service_name: str = "session-service"
+    service_port: int = 8001
+    database_url: str = "sqlite:///./session_service.db"
+    runtime_service_url: str = "http://127.0.0.1:8003"

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

@@ -0,0 +1 @@
+

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

@@ -0,0 +1,7 @@
+from core_db import Base
+
+from .message import Message
+from .run_request import RunRequest
+from .session import Session
+
+__all__ = ["Base", "Message", "RunRequest", "Session"]

+ 18 - 0
services/session-service/app/db/models/message.py

@@ -0,0 +1,18 @@
+from sqlalchemy import Integer, String, Text
+from sqlalchemy.dialects.sqlite import JSON
+from sqlalchemy.orm import Mapped, mapped_column
+
+from core_db import AuditMixin, Base, TenantMixin, VersionMixin
+
+
+class Message(TenantMixin, AuditMixin, VersionMixin, Base):
+    __tablename__ = "message"
+
+    session_id: Mapped[str] = mapped_column(String(36), index=True)
+    turn_id: Mapped[str | None] = mapped_column(String(36), nullable=True, index=True)
+    role: Mapped[str] = mapped_column(String(32))
+    content_type: Mapped[str] = mapped_column(String(32))
+    content_text: Mapped[str | None] = mapped_column(Text, nullable=True)
+    content_json: Mapped[dict | None] = mapped_column(JSON, nullable=True)
+    token_count: Mapped[int | None] = mapped_column(Integer, nullable=True)
+

+ 17 - 0
services/session-service/app/db/models/run_request.py

@@ -0,0 +1,17 @@
+from sqlalchemy import String
+from sqlalchemy.dialects.sqlite import JSON
+from sqlalchemy.orm import Mapped, mapped_column
+
+from core_db import AuditMixin, Base, TenantMixin, VersionMixin
+
+
+class RunRequest(TenantMixin, AuditMixin, VersionMixin, Base):
+    __tablename__ = "run_request"
+
+    session_id: Mapped[str] = mapped_column(String(36), index=True)
+    app_version_id: Mapped[str] = mapped_column(String(36))
+    workflow_version_id: Mapped[str] = mapped_column(String(36))
+    trigger_type: Mapped[str] = mapped_column(String(32))
+    request_payload_json: Mapped[dict | None] = mapped_column(JSON, nullable=True)
+    request_status: Mapped[str] = mapped_column(String(32), default="accepted")
+

+ 20 - 0
services/session-service/app/db/models/session.py

@@ -0,0 +1,20 @@
+from datetime import datetime
+
+from sqlalchemy import DateTime, String
+from sqlalchemy.orm import Mapped, mapped_column
+
+from core_db import AuditMixin, Base, TenantMixin, VersionMixin
+
+
+class Session(TenantMixin, AuditMixin, VersionMixin, Base):
+    __tablename__ = "session"
+
+    app_id: Mapped[str] = mapped_column(String(36), index=True)
+    user_id: Mapped[str] = mapped_column(String(36), index=True)
+    channel_type: Mapped[str] = mapped_column(String(32))
+    session_status: Mapped[str] = mapped_column(String(32), default="active")
+    title: Mapped[str | None] = mapped_column(String(256), nullable=True)
+    started_time: Mapped[datetime | None] = mapped_column(DateTime, nullable=True)
+    last_active_time: Mapped[datetime | None] = mapped_column(DateTime, nullable=True)
+    closed_time: Mapped[datetime | None] = mapped_column(DateTime, nullable=True)
+

+ 30 - 0
services/session-service/app/db/session.py

@@ -0,0 +1,30 @@
+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 SessionServiceSettings
+
+
+def build_session_factory(
+    settings: SessionServiceSettings | None = None,
+) -> sessionmaker[Session]:
+    resolved_settings = settings or SessionServiceSettings()
+    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/session-service/app/domain/__init__.py

@@ -0,0 +1 @@
+

+ 118 - 0
services/session-service/app/domain/repositories.py

@@ -0,0 +1,118 @@
+from datetime import datetime
+
+from sqlalchemy import select
+from sqlalchemy.orm import Session
+
+from app.db.models import Message, RunRequest, Session as SessionModel
+
+
+class SessionRepository:
+    def __init__(self, db: Session) -> None:
+        self.db = db
+
+    def create(
+        self,
+        *,
+        tenant_id: str,
+        app_id: str,
+        user_id: str,
+        channel_type: str,
+        title: str | None,
+    ) -> SessionModel:
+        entity = SessionModel(
+            tenant_id=tenant_id,
+            app_id=app_id,
+            user_id=user_id,
+            channel_type=channel_type,
+            title=title,
+            started_time=datetime.utcnow(),
+            last_active_time=datetime.utcnow(),
+        )
+        self.db.add(entity)
+        self.db.commit()
+        self.db.refresh(entity)
+        return entity
+
+    def list_by_scope(self, *, tenant_id: str, app_id: str | None = None) -> list[SessionModel]:
+        stmt = select(SessionModel).where(SessionModel.tenant_id == tenant_id)
+        if app_id:
+            stmt = stmt.where(SessionModel.app_id == app_id)
+        return list(self.db.scalars(stmt))
+
+
+class MessageRepository:
+    def __init__(self, db: Session) -> None:
+        self.db = db
+
+    def create(
+        self,
+        *,
+        tenant_id: str,
+        session_id: str,
+        turn_id: str | None,
+        role: str,
+        content_type: str,
+        content_text: str | None,
+        content_json: dict | None,
+    ) -> Message:
+        entity = Message(
+            tenant_id=tenant_id,
+            session_id=session_id,
+            turn_id=turn_id,
+            role=role,
+            content_type=content_type,
+            content_text=content_text,
+            content_json=content_json,
+        )
+        self.db.add(entity)
+        self.db.commit()
+        self.db.refresh(entity)
+        return entity
+
+    def list_by_session(self, *, tenant_id: str, session_id: str) -> list[Message]:
+        stmt = (
+            select(Message)
+            .where(Message.tenant_id == tenant_id)
+            .where(Message.session_id == session_id)
+            .order_by(Message.created_time.asc())
+        )
+        return list(self.db.scalars(stmt))
+
+
+class RunRequestRepository:
+    def __init__(self, db: Session) -> None:
+        self.db = db
+
+    def create(
+        self,
+        *,
+        tenant_id: str,
+        session_id: str,
+        app_version_id: str,
+        workflow_version_id: str,
+        trigger_type: str,
+        request_payload_json: dict | None,
+        request_status: str,
+    ) -> RunRequest:
+        entity = RunRequest(
+            tenant_id=tenant_id,
+            session_id=session_id,
+            app_version_id=app_version_id,
+            workflow_version_id=workflow_version_id,
+            trigger_type=trigger_type,
+            request_payload_json=request_payload_json,
+            request_status=request_status,
+        )
+        self.db.add(entity)
+        self.db.commit()
+        self.db.refresh(entity)
+        return entity
+
+    def list_by_session(self, *, tenant_id: str, session_id: str) -> list[RunRequest]:
+        stmt = (
+            select(RunRequest)
+            .where(RunRequest.tenant_id == tenant_id)
+            .where(RunRequest.session_id == session_id)
+            .order_by(RunRequest.created_time.desc())
+        )
+        return list(self.db.scalars(stmt))

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

@@ -0,0 +1 @@
+

+ 25 - 0
services/session-service/app/infrastructure/runtime_client.py

@@ -0,0 +1,25 @@
+import httpx
+
+from core_domain import RunBootstrapContract, RunCreateContract
+
+
+class RuntimeServiceClientError(Exception):
+    pass
+
+
+class RuntimeServiceClient:
+    def __init__(self, base_url: str, timeout_seconds: float = 10.0) -> None:
+        self.base_url = base_url.rstrip("/")
+        self.timeout_seconds = timeout_seconds
+
+    def create_run(self, payload: RunCreateContract) -> RunBootstrapContract:
+        try:
+            with httpx.Client(timeout=self.timeout_seconds) as client:
+                response = client.post(
+                    f"{self.base_url}/runtime/runs",
+                    json=payload.model_dump(mode="json"),
+                )
+                response.raise_for_status()
+                return RunBootstrapContract.model_validate(response.json())
+        except httpx.HTTPError as exc:
+            raise RuntimeServiceClientError(f"runtime-service request failed: {exc}") from exc

+ 4 - 0
services/session-service/app/main.py

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

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

@@ -0,0 +1 @@
+

+ 34 - 0
services/session-service/app/schemas/message.py

@@ -0,0 +1,34 @@
+from datetime import datetime
+from typing import TYPE_CHECKING
+
+from pydantic import BaseModel, Field
+from core_shared import JSONValue
+
+if TYPE_CHECKING:
+    from app.db.models import Message
+
+
+class MessageCreateRequest(BaseModel):
+    tenant_id: str
+    session_id: str
+    turn_id: str | None = None
+    role: str
+    content_type: str = "text"
+    content_text: str | None = None
+    content_json: dict[str, JSONValue] = Field(default_factory=dict)
+
+
+class MessageResponse(BaseModel):
+    id: str
+    tenant_id: str
+    session_id: str
+    turn_id: str | None = None
+    role: str
+    content_type: str
+    content_text: str | None = None
+    content_json: dict[str, JSONValue] | None = None
+    created_time: datetime
+
+    @classmethod
+    def from_entity(cls, entity: "Message") -> "MessageResponse":
+        return cls.model_validate(entity, from_attributes=True)

+ 69 - 0
services/session-service/app/schemas/run_request.py

@@ -0,0 +1,69 @@
+from datetime import datetime
+from typing import TYPE_CHECKING
+
+from pydantic import BaseModel, Field
+from core_domain import InitialNodeContract, RunBootstrapContract, RunCreateContract
+from core_shared import JSONValue
+
+if TYPE_CHECKING:
+    from app.db.models import RunRequest
+
+
+class InitialNodeRequest(InitialNodeContract):
+    pass
+
+
+class RunRequestCreateRequest(BaseModel):
+    tenant_id: str
+    session_id: str
+    app_version_id: str
+    workflow_version_id: str
+    trigger_type: str = "chat"
+    request_payload_json: dict[str, JSONValue] = Field(default_factory=dict)
+    request_status: str = "accepted"
+
+
+class RunRequestResponse(BaseModel):
+    id: str
+    tenant_id: str
+    session_id: str
+    app_version_id: str
+    workflow_version_id: str
+    trigger_type: str
+    request_payload_json: dict[str, JSONValue] | None = None
+    request_status: str
+    created_time: datetime
+
+    @classmethod
+    def from_entity(cls, entity: "RunRequest") -> "RunRequestResponse":
+        return cls.model_validate(entity, from_attributes=True)
+
+
+class DispatchRunRequest(BaseModel):
+    tenant_id: str
+    session_id: str
+    app_id: str
+    app_version_id: str
+    workflow_id: str
+    workflow_version_id: str
+    trigger_type: str = "chat"
+    priority: int = 0
+    request_payload_json: dict[str, JSONValue] = Field(default_factory=dict)
+    initial_node: InitialNodeRequest | None = None
+
+
+class DispatchRunResponse(BaseModel):
+    run_request: RunRequestResponse
+    runtime_response: RunBootstrapContract
+
+    @classmethod
+    def from_parts(
+        cls,
+        *,
+        run_request: "RunRequest",
+        runtime_response: RunBootstrapContract,
+    ) -> "DispatchRunResponse":
+        return cls(
+            run_request=RunRequestResponse.from_entity(run_request),
+            runtime_response=runtime_response,
+        )

+ 32 - 0
services/session-service/app/schemas/session.py

@@ -0,0 +1,32 @@
+from datetime import datetime
+from typing import TYPE_CHECKING
+
+from pydantic import BaseModel
+
+if TYPE_CHECKING:
+    from app.db.models import Session as SessionModel
+
+
+class SessionCreateRequest(BaseModel):
+    tenant_id: str
+    app_id: str
+    user_id: str
+    channel_type: str = "web"
+    title: str | None = None
+
+
+class SessionResponse(BaseModel):
+    id: str
+    tenant_id: str
+    app_id: str
+    user_id: str
+    channel_type: str
+    session_status: str
+    title: str | None = None
+    started_time: datetime | None = None
+    last_active_time: datetime | None = None
+    created_time: datetime
+
+    @classmethod
+    def from_entity(cls, entity: "SessionModel") -> "SessionResponse":
+        return cls.model_validate(entity, from_attributes=True)

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

@@ -0,0 +1,26 @@
+[build-system]
+requires = ["setuptools>=68"]
+build-backend = "setuptools.build_meta"
+
+[project]
+name = "session-service"
+version = "0.1.0"
+description = "Session 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 = ["."]

+ 37 - 0
services/tool-service/alembic.ini

@@ -0,0 +1,37 @@
+[alembic]
+script_location = alembic
+prepend_sys_path = .
+sqlalchemy.url = sqlite:///./tool_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
+

+ 42 - 0
services/tool-service/alembic/env.py

@@ -0,0 +1,42 @@
+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/tool-service/alembic/versions/.gitkeep

@@ -0,0 +1 @@
+

+ 97 - 0
services/tool-service/alembic/versions/20260422_0001_init_tool_models.py

@@ -0,0 +1,97 @@
+"""init tool models
+
+Revision ID: 20260422_0001
+Revises:
+Create Date: 2026-04-22 17:20:00
+"""
+
+from collections.abc import Sequence
+
+from alembic import op
+import sqlalchemy as sa
+
+
+revision: str = "20260422_0001"
+down_revision: str | None = None
+branch_labels: Sequence[str] | None = None
+depends_on: Sequence[str] | None = None
+
+
+def upgrade() -> None:
+    op.create_table(
+        "tool_definition",
+        sa.Column("plugin_id", sa.String(length=36), nullable=True),
+        sa.Column("code", sa.String(length=64), nullable=False),
+        sa.Column("name", sa.String(length=128), nullable=False),
+        sa.Column("tool_type", sa.String(length=32), nullable=False),
+        sa.Column("description", 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_tool_definition_code", "tool_definition", ["code"], unique=False)
+    op.create_index("ix_tool_definition_tenant_id", "tool_definition", ["tenant_id"], unique=False)
+
+    op.create_table(
+        "tool_version",
+        sa.Column("tool_id", sa.String(length=36), nullable=False),
+        sa.Column("version_no", sa.Integer(), nullable=False),
+        sa.Column("input_schema_json", sa.JSON(), nullable=True),
+        sa.Column("output_schema_json", sa.JSON(), nullable=True),
+        sa.Column("invoke_config_json", sa.JSON(), nullable=True),
+        sa.Column("timeout_ms", sa.Integer(), nullable=True),
+        sa.Column("retry_policy_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_tool_version_tenant_id", "tool_version", ["tenant_id"], unique=False)
+    op.create_index("ix_tool_version_tool_id", "tool_version", ["tool_id"], unique=False)
+
+    op.create_table(
+        "tool_binding",
+        sa.Column("app_id", sa.String(length=36), nullable=False),
+        sa.Column("tool_version_id", sa.String(length=36), nullable=False),
+        sa.Column("credential_id", sa.String(length=36), nullable=True),
+        sa.Column("binding_scope", sa.String(length=32), nullable=False),
+        sa.Column("enabled", sa.Boolean(), nullable=False),
+        sa.Column("config_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_tool_binding_app_id", "tool_binding", ["app_id"], unique=False)
+    op.create_index("ix_tool_binding_tenant_id", "tool_binding", ["tenant_id"], unique=False)
+
+
+def downgrade() -> None:
+    op.drop_index("ix_tool_binding_tenant_id", table_name="tool_binding")
+    op.drop_index("ix_tool_binding_app_id", table_name="tool_binding")
+    op.drop_table("tool_binding")
+
+    op.drop_index("ix_tool_version_tool_id", table_name="tool_version")
+    op.drop_index("ix_tool_version_tenant_id", table_name="tool_version")
+    op.drop_table("tool_version")
+
+    op.drop_index("ix_tool_definition_tenant_id", table_name="tool_definition")
+    op.drop_index("ix_tool_definition_code", table_name="tool_definition")
+    op.drop_table("tool_definition")
+

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

@@ -0,0 +1 @@
+

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

@@ -0,0 +1 @@
+

+ 91 - 0
services/tool-service/app/api/routes.py

@@ -0,0 +1,91 @@
+from fastapi import APIRouter, Depends, Query
+from sqlalchemy import text
+from sqlalchemy.orm import Session
+
+from core_domain import ServiceHealth
+from app.application.services import ToolApplicationService
+from app.db.session import get_db
+from app.domain.repositories import ToolBindingRepository, ToolDefinitionRepository, ToolVersionRepository
+from app.schemas.tool import (
+    ToolBindingCreateRequest,
+    ToolBindingResponse,
+    ToolCreateRequest,
+    ToolResponse,
+    ToolVersionCreateRequest,
+    ToolVersionResponse,
+)
+
+router = APIRouter()
+
+
+def get_tool_application_service(db: Session = Depends(get_db)) -> ToolApplicationService:
+    return ToolApplicationService(
+        tool_definition_repository=ToolDefinitionRepository(db),
+        tool_version_repository=ToolVersionRepository(db),
+        tool_binding_repository=ToolBindingRepository(db),
+    )
+
+
+@router.get("/health", response_model=ServiceHealth)
+def health_check(db: Session = Depends(get_db)) -> ServiceHealth:
+    db.execute(text("SELECT 1"))
+    return ServiceHealth(service="tool-service", status="ok", database="ok")
+
+
+@router.post("", response_model=ToolResponse)
+def create_tool_definition(
+    payload: ToolCreateRequest,
+    service: ToolApplicationService = Depends(get_tool_application_service),
+) -> ToolResponse:
+    entity = service.create_tool_definition(payload)
+    return ToolResponse.from_entity(entity)
+
+
+@router.get("", response_model=list[ToolResponse])
+def list_tool_definitions(
+    tenant_id: str = Query(...),
+    service: ToolApplicationService = Depends(get_tool_application_service),
+) -> list[ToolResponse]:
+    return [ToolResponse.from_entity(item) for item in service.list_tool_definitions(tenant_id)]
+
+
+@router.post("/versions", response_model=ToolVersionResponse)
+def create_tool_version(
+    payload: ToolVersionCreateRequest,
+    service: ToolApplicationService = Depends(get_tool_application_service),
+) -> ToolVersionResponse:
+    entity = service.create_tool_version(payload)
+    return ToolVersionResponse.from_entity(entity)
+
+
+@router.get("/versions", response_model=list[ToolVersionResponse])
+def list_tool_versions(
+    tenant_id: str = Query(...),
+    tool_id: str = Query(...),
+    service: ToolApplicationService = Depends(get_tool_application_service),
+) -> list[ToolVersionResponse]:
+    return [
+        ToolVersionResponse.from_entity(item)
+        for item in service.list_tool_versions(tenant_id=tenant_id, tool_id=tool_id)
+    ]
+
+
+@router.post("/bindings", response_model=ToolBindingResponse)
+def create_tool_binding(
+    payload: ToolBindingCreateRequest,
+    service: ToolApplicationService = Depends(get_tool_application_service),
+) -> ToolBindingResponse:
+    entity = service.create_tool_binding(payload)
+    return ToolBindingResponse.from_entity(entity)
+
+
+@router.get("/bindings", response_model=list[ToolBindingResponse])
+def list_tool_bindings(
+    tenant_id: str = Query(...),
+    app_id: str | None = Query(default=None),
+    service: ToolApplicationService = Depends(get_tool_application_service),
+) -> list[ToolBindingResponse]:
+    return [
+        ToolBindingResponse.from_entity(item)
+        for item in service.list_tool_bindings(tenant_id=tenant_id, app_id=app_id)
+    ]

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

@@ -0,0 +1 @@
+

+ 61 - 0
services/tool-service/app/application/services.py

@@ -0,0 +1,61 @@
+from app.db.models import ToolBinding, ToolDefinition, ToolVersion
+from app.domain.repositories import ToolBindingRepository, ToolDefinitionRepository, ToolVersionRepository
+from app.schemas.tool import (
+    ToolBindingCreateRequest,
+    ToolCreateRequest,
+    ToolVersionCreateRequest,
+)
+
+
+class ToolApplicationService:
+    def __init__(
+        self,
+        tool_definition_repository: ToolDefinitionRepository,
+        tool_version_repository: ToolVersionRepository,
+        tool_binding_repository: ToolBindingRepository,
+    ) -> None:
+        self.tool_definition_repository = tool_definition_repository
+        self.tool_version_repository = tool_version_repository
+        self.tool_binding_repository = tool_binding_repository
+
+    def create_tool_definition(self, payload: ToolCreateRequest) -> ToolDefinition:
+        return self.tool_definition_repository.create(
+            tenant_id=payload.tenant_id,
+            plugin_id=payload.plugin_id,
+            code=payload.code,
+            name=payload.name,
+            tool_type=payload.tool_type,
+            description=payload.description,
+        )
+
+    def list_tool_definitions(self, tenant_id: str) -> list[ToolDefinition]:
+        return self.tool_definition_repository.list_by_tenant(tenant_id)
+
+    def create_tool_version(self, payload: ToolVersionCreateRequest) -> ToolVersion:
+        return self.tool_version_repository.create(
+            tenant_id=payload.tenant_id,
+            tool_id=payload.tool_id,
+            input_schema_json=payload.input_schema_json,
+            output_schema_json=payload.output_schema_json,
+            invoke_config_json=payload.invoke_config_json,
+            timeout_ms=payload.timeout_ms,
+            retry_policy_json=payload.retry_policy_json,
+        )
+
+    def list_tool_versions(self, tenant_id: str, tool_id: str) -> list[ToolVersion]:
+        return self.tool_version_repository.list_by_tool(tenant_id=tenant_id, tool_id=tool_id)
+
+    def create_tool_binding(self, payload: ToolBindingCreateRequest) -> ToolBinding:
+        return self.tool_binding_repository.create(
+            tenant_id=payload.tenant_id,
+            app_id=payload.app_id,
+            tool_version_id=payload.tool_version_id,
+            credential_id=payload.credential_id,
+            binding_scope=payload.binding_scope,
+            enabled=payload.enabled,
+            config_json=payload.config_json,
+        )
+
+    def list_tool_bindings(self, tenant_id: str, app_id: str | None = None) -> list[ToolBinding]:
+        return self.tool_binding_repository.list_by_scope(tenant_id=tenant_id, app_id=app_id)
+

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

@@ -0,0 +1 @@
+

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

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

Niektóre pliki nie zostały wyświetlone z powodu dużej ilości zmienionych plików