Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ release tags and PEP 440 for the Python package.
between Agents and governed Office stations.
- Versioned Company Pack SDK definition and Catalog boundary, with Music Studio moved into a
discoverable scenario package while retaining its existing API and compatibility import.
- Scenario-owned Music Studio runtime, provider adapters, HTTP routes, and workspace assets with
unchanged public URLs and compatibility re-exports for pre-alpha Python imports.

## 0.1.0-alpha.1 — 2026-07-27

Expand Down
28 changes: 18 additions & 10 deletions docs/architecture/modules/pack-sdk-boundary.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Company Pack SDK and Scenario Boundary

Status: Implemented phase 1
Status: Implemented phase 2

AgentMesh keeps the operating runtime independent from business scenarios. The runtime owns Tasks,
Agents, governance, persistence, Pack installation, compatibility checks, upgrades, and audit.
Expand All @@ -18,16 +18,24 @@ The first stable in-process SDK surface is `agentmesh.packs.sdk`:
- the API resolves Music Studio through the built-in Catalog and passes its definition into generic
preview, installation, upgrade-preview, and upgrade operations.

Music Studio now lives under `agentmesh.packs.music_studio`. The old
`agentmesh.templates.music_studio` module is a compatibility-only re-export so downstream imports
do not break during the transition.
Music Studio now lives under `agentmesh.packs.music_studio`. It owns its definition, runtime
service, deterministic provider adapters, HTTP routes, and focused workspace assets. The API and
bootstrap modules are composition roots: they explicitly attach the scenario to AgentMesh without
moving scenario behavior back into the generic application layer.

The old `agentmesh.templates.music_studio`,
`agentmesh.application.music_studio_services`, `agentmesh.integrations.music.deterministic`, and
`agentmesh.api.music_studio_routes` modules are compatibility-only re-exports so downstream
imports do not break during the transition. The public `/api/v1/music-studio`, `/music-studio`,
and `/console/assets/music-studio.*` URLs are unchanged.

## Dependency direction

```text
Music Studio definition -> Pack SDK -> Company Pack domain
Company Template API ----> Catalog ----> definition
Company Pack service ----> Pack SDK contract
API/bootstrap roots ------> Music Studio runtime/routes/console
```

The Company Pack service must not import a concrete scenario. Scenario configuration validation
Expand All @@ -43,9 +51,9 @@ that exposes the same definition contract after a separate trust and loading des

## Remaining separation work

Phase 1 separates the declarative Pack and configuration contract. Later phases can move the
Music Studio workflow service, HTTP routes, provider adapters, and static UI into a separately
versioned distribution. The older Market Intelligence template and Operations helpers must also
move onto the same definition contract before the whole application layer is scenario-neutral.
Physical repository separation should wait until the SDK compatibility policy and external Pack
loading/trust model are stable.
Phases 1 and 2 separate both the declarative contract and the complete Music Studio implementation
inside the Python package. A later phase can publish that directory as a separately versioned
distribution once the runtime extension protocol and signed external Pack loading/trust model are
stable. The older Market Intelligence template and Operations helpers must also move onto the same
definition contract before the whole application layer is scenario-neutral. Until then, physical
repository separation would create release coupling without a safe installation boundary.
12 changes: 12 additions & 0 deletions docs/implementation-status.md
Original file line number Diff line number Diff line change
Expand Up @@ -181,6 +181,18 @@ Company Pack SDK separation phase 1 on 2026-08-03 additionally:
- kept workflow-service, route, provider, and UI distribution extraction as a later phase after
the SDK compatibility and external trust/loading contracts stabilize.

Company Pack SDK separation phase 2 on 2026-08-03 additionally:

- passed 419 non-PostgreSQL tests at 83.11% line coverage (gate: 80%);
- moved the Music Studio runtime service, deterministic provider adapters, HTTP routes, and all
focused-workspace assets under `agentmesh.packs.music_studio`;
- reduced the generic API and bootstrap modules to explicit scenario composition roots while
preserving every existing HTTP URL;
- retained compatibility-only modules for all pre-alpha import paths and added identity tests so
downstream users receive the same implementation during the transition;
- left external executable Pack loading deliberately unsupported until a signed trust and runtime
extension protocol is specified.

Market Intelligence Studio baseline verification on 2026-07-30 additionally:

- passed 390 non-PostgreSQL tests at 82.71% line coverage (gate: 80%);
Expand Down
5 changes: 4 additions & 1 deletion src/agentmesh/api/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,6 @@
from agentmesh.api.identity_routes import router as identity_router
from agentmesh.api.mcp_routes import registry_router as mcp_registry_router
from agentmesh.api.mcp_routes import router as mcp_router
from agentmesh.api.music_studio_routes import router as music_studio_router
from agentmesh.api.office_routes import router as office_router
from agentmesh.api.organizational_memory_routes import (
router as organizational_memory_router,
Expand Down Expand Up @@ -111,6 +110,8 @@
TaskNotFound,
ToolInvocationFailed,
)
from agentmesh.packs.music_studio.console import register_music_studio_console
from agentmesh.packs.music_studio.routes import router as music_studio_router


def create_app(container: ApplicationContainer | None = None) -> FastAPI:
Expand Down Expand Up @@ -154,6 +155,8 @@ async def lifespan(app: FastAPI) -> AsyncIterator[None]:
application.include_router(policy_router)
application.include_router(quota_router)
application.include_router(office_router)
# Scenario-owned asset routes must be registered before the core static mount.
register_music_studio_console(application)
register_console(application)
_register_error_handlers(application)
return application
Expand Down
19 changes: 8 additions & 11 deletions src/agentmesh/api/console.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,33 +24,26 @@ def register_console(application: FastAPI) -> None:
def console_index() -> FileResponse:
return FileResponse(
CONSOLE_DIRECTORY / "index.html",
headers=_console_headers(),
headers=console_headers(),
)

@application.get("/world", include_in_schema=False)
def world_index() -> FileResponse:
return FileResponse(
CONSOLE_DIRECTORY / "world.html",
headers=_console_headers(),
)

@application.get("/music-studio", include_in_schema=False)
def music_studio_index() -> FileResponse:
return FileResponse(
CONSOLE_DIRECTORY / "music-studio.html",
headers=_console_headers(),
headers=console_headers(),
)

@application.get("/world-3d", include_in_schema=False)
def world_3d_index(request: Request) -> FileResponse:
request.app.state.container.feature_gates.require(Feature.OFFICE_3D)
return FileResponse(
CONSOLE_DIRECTORY / "world3d.html",
headers=_console_headers(),
headers=console_headers(),
)


def _console_headers() -> dict[str, str]:
def console_headers() -> dict[str, str]:
return {
"Cache-Control": "no-store",
"Content-Security-Policy": (
Expand All @@ -61,3 +54,7 @@ def _console_headers() -> dict[str, str]:
"Referrer-Policy": "no-referrer",
"X-Content-Type-Options": "nosniff",
}


# Kept for callers that used the original private helper during pre-alpha.
_console_headers = console_headers
212 changes: 23 additions & 189 deletions src/agentmesh/api/music_studio_routes.py
Original file line number Diff line number Diff line change
@@ -1,191 +1,25 @@
from typing import Annotated
from uuid import UUID

from fastapi import APIRouter, Depends, Header, Request, status
from pydantic import BaseModel, Field

from agentmesh.api.business_object_schemas import BusinessObjectSnapshotResponse
from agentmesh.api.feature_routes import require_feature
from agentmesh.api.schemas import TaskResponse
from agentmesh.api.security import PrincipalDependency, require_permission
from agentmesh.application.music_studio_services import MusicStudioService
from agentmesh.domain.identity import Permission
from agentmesh.features import Feature

router = APIRouter(
prefix="/api/v1/music-studio",
tags=["music-studio"],
dependencies=[
Depends(require_feature(Feature.COMPANY_PACKS)),
Depends(require_feature(Feature.ARTIFACT_SERVICE)),
],
"""Compatibility imports for the Music Studio HTTP API."""

from agentmesh.packs.music_studio.routes import (
CreateMusicProjectRequest,
MusicCandidateResponse,
MusicProjectLaunchResponse,
MusicProjectResultResponse,
RequestMusicRevisionRequest,
SelectMusicCandidateRequest,
ServiceDependency,
get_service,
router,
)


class CreateMusicProjectRequest(BaseModel):
title: str = Field(min_length=1, max_length=160)
audience: str = Field(min_length=1, max_length=500)
language: str = Field(min_length=2, max_length=32)
mood: str = Field(min_length=1, max_length=160)
themes: list[str] = Field(min_length=1, max_length=8)
genre_attributes: list[str] = Field(min_length=1, max_length=8)
max_rounds: int = Field(default=3, ge=1, le=5)


class MusicProjectLaunchResponse(BaseModel):
task: TaskResponse
project: BusinessObjectSnapshotResponse


class RequestMusicRevisionRequest(BaseModel):
failed_criterion: str = Field(min_length=1, max_length=500)
requested_change: str = Field(min_length=1, max_length=1000)


class SelectMusicCandidateRequest(BaseModel):
candidate_id: UUID


class MusicCandidateResponse(BaseModel):
candidate_id: UUID
review_id: UUID
variant: str
audio_artifact_id: UUID
audio_version_id: UUID
overall_score: int
findings: list[str]
selected: bool


class MusicProjectResultResponse(BaseModel):
task_id: UUID
status: str
project_id: UUID
title: str
current_round: int
max_rounds: int
candidate_id: UUID | None
review_id: UUID | None
release_id: UUID | None
audio_artifact_id: UUID | None
audio_version_id: UUID | None
overall_score: int | None
findings: list[str]
candidates: list[MusicCandidateResponse]
package_artifact_id: UUID | None
package_version_id: UUID | None
message: str | None


def get_service(request: Request) -> MusicStudioService:
return request.app.state.container.music_studio_service


ServiceDependency = Annotated[MusicStudioService, Depends(get_service)]
IdempotencyHeader = Annotated[str, Header(alias="Idempotency-Key", max_length=255)]


@router.post(
"/projects",
response_model=MusicProjectLaunchResponse,
status_code=status.HTTP_201_CREATED,
dependencies=[
Depends(require_permission(Permission.COMPANY_MANAGE)),
Depends(require_permission(Permission.TASK_CREATE)),
Depends(require_permission(Permission.TASK_OPERATE)),
],
)
def create_project(
payload: CreateMusicProjectRequest,
service: ServiceDependency,
principal: PrincipalDependency,
idempotency_key: IdempotencyHeader,
) -> MusicProjectLaunchResponse:
result = service.launch(
**payload.model_dump(),
requested_by=principal.principal_id,
idempotency_key=idempotency_key,
)
return MusicProjectLaunchResponse(
task=TaskResponse.from_aggregate(result.task),
project=BusinessObjectSnapshotResponse.from_snapshot(result.project),
)


@router.get("/projects/{task_id}", response_model=MusicProjectResultResponse)
def get_project(task_id: UUID, service: ServiceDependency) -> MusicProjectResultResponse:
return MusicProjectResultResponse.model_validate(service.status(task_id), from_attributes=True)


@router.post(
"/projects/{task_id}/materialize",
response_model=MusicProjectResultResponse,
dependencies=[Depends(require_permission(Permission.TASK_OPERATE))],
)
def materialize_project(
task_id: UUID,
service: ServiceDependency,
principal: PrincipalDependency,
) -> MusicProjectResultResponse:
return MusicProjectResultResponse.model_validate(
service.materialize(task_id, actor=principal.principal_id), from_attributes=True
)


@router.post(
"/projects/{task_id}/select",
response_model=MusicProjectResultResponse,
dependencies=[Depends(require_permission(Permission.COMPANY_MANAGE))],
)
def select_project_candidate(
task_id: UUID,
payload: SelectMusicCandidateRequest,
service: ServiceDependency,
principal: PrincipalDependency,
) -> MusicProjectResultResponse:
return MusicProjectResultResponse.model_validate(
service.select_candidate(
task_id,
candidate_id=payload.candidate_id,
actor=principal.principal_id,
),
from_attributes=True,
)


@router.post(
"/projects/{task_id}/approve",
response_model=MusicProjectResultResponse,
dependencies=[Depends(require_permission(Permission.COMPANY_MANAGE))],
)
def approve_project(
task_id: UUID,
service: ServiceDependency,
principal: PrincipalDependency,
) -> MusicProjectResultResponse:
return MusicProjectResultResponse.model_validate(
service.approve(task_id, actor=principal.principal_id), from_attributes=True
)


@router.post(
"/projects/{task_id}/revision",
response_model=MusicProjectResultResponse,
dependencies=[Depends(require_permission(Permission.COMPANY_MANAGE))],
)
def request_project_revision(
task_id: UUID,
payload: RequestMusicRevisionRequest,
service: ServiceDependency,
principal: PrincipalDependency,
idempotency_key: IdempotencyHeader,
) -> MusicProjectResultResponse:
return MusicProjectResultResponse.model_validate(
service.request_revision(
task_id,
**payload.model_dump(),
actor=principal.principal_id,
idempotency_key=idempotency_key,
),
from_attributes=True,
)
__all__ = [
"CreateMusicProjectRequest",
"MusicCandidateResponse",
"MusicProjectLaunchResponse",
"MusicProjectResultResponse",
"RequestMusicRevisionRequest",
"SelectMusicCandidateRequest",
"ServiceDependency",
"get_service",
"router",
]
Loading