Skip to main content
Version: 2026.1

Development

Developer workflow reference — how to run the bundle locally, what commands each subsystem exposes, how to trigger reloads. For end-to-end install, see Installation. For when things go wrong, see Troubleshooting. For authentication details (they used to live here) see Architecture → Authentication.

Docker setup

The default docker-compose.yaml runs agent-server in production mode — compiled dist/server.js, prod-only dependencies, no source bind mount. That is correct for demos and staging.

For local agent-server development with hot reload on .ts changes, create a docker-compose.override.yaml next to the main compose file:

# docker-compose.override.yaml — bind-mounts source and runs tsx watch
services:
agent-server:
user: '1000:1000'
# Override the Dockerfile build with a bare node image + bind-mounted source
image: node:22-slim
build: !reset null
command: sh -c "npm ci && npx tsx watch src/server.ts"
working_dir: /app
environment:
NODE_ENV: development
# Read/sandbox staged files from a TOP-LEVEL path, not /app/uploads — see
# the staged-files note below.
AGENT_SERVER_UPLOAD_DIR: /agent-uploads
volumes:
- ./dev/pimcore/pimcore-agent-bundle/agent-server:/app
# Preserve runtime state mounts from the main compose file (list fields
# are replaced, not merged, so they must be repeated here).
- ./var/tmp/copilot-state:/app/.copilot-state
# Staged files: TOP-LEVEL mount (a sibling of /app, NOT nested inside it).
- ./var/tmp/agent-uploads:/agent-uploads
# node:22-slim ships no CA certs; the Copilot CLI validates its auth
# token against the system trust store, so mount the host's read-only.
# (The production Dockerfile installs ca-certificates instead.)
- /etc/ssl/certs:/etc/ssl/certs:ro

php:
user: '1000:1000' # set to your uid:gid
environment:
# Hand the agent /agent-uploads paths, matching the agent-server mount above.
AGENT_NODE_STAGING_BASE_DIR: /agent-uploads

supervisord:
user: '1000:1000' # set to your uid:gid

npm ci installs directly into the bind-mounted source directory as uid 1000, so node_modules ends up host-side with correct ownership and is available to your IDE for autocompletion. No named volume is used to avoid Docker daemon creating the mount point as root.

Before the first start: create the host directories that back the mounts, so Docker does not create them as root:

mkdir -p var/tmp/copilot-state var/tmp/agent-uploads
mkdir -p dev/pimcore/pimcore-agent-bundle/agent-server/var

Why staged files mount at /agent-uploads, not /app/uploads. The production compose mounts the host upload dir at /app/uploads. Reusing that path in dev is a trap: the override bind-mounts the whole source dir to /app, so /app/uploads would be a bind mount nested inside another bind mount. On some storage drivers — notably ZFS — that inner mount silently detaches at runtime (no container restart; Docker's config still lists it, but the kernel has dropped it). Once it's gone, every file stage_file_set writes is invisible to the agent, which then reports staged files as "inaccessible" and falls back to guessed layout conventions.

The fix is to avoid the nesting: mount the upload dir at a top-level path (/agent-uploads) and point both sides at it — the agent-server via AGENT_SERVER_UPLOAD_DIR, and the PHP MCP tools via AGENT_NODE_STAGING_BASE_DIR (the path string they hand the agent; it must equal the agent-server's upload dir). Production is unaffected: both default to /app/uploads, where the single, non-nested prod mount is stable. Verify the dev mount with:

docker compose exec agent-server ls /agent-uploads   # lists per-session dirs, not an error

If staging ever breaks anyway, docker compose restart agent-server re-attaches the mount in a few seconds — a full Docker restart is never needed.

With the override in place, TypeScript changes are picked up automatically via tsx watch — no container restart, no rebuild. Agent YAML edits still need a reload:

curl -X POST -H "Authorization: Bearer $AGENT_SERVER_ADMIN_TOKEN" \
http://localhost/agent-server/api/admin/reload-agents

Tip: to wipe all Copilot CLI session state, run rm -rf var/tmp/copilot-state/* and restart the container.

Agent-server (Node.js)

Working directory: agent-server/. Commands require the dev override shown above — the production image strips devDependencies (npm ci --omit=dev) and does not contain tsx, eslint, vitest, or typescript.

# Inside the container
docker compose exec agent-server npm test # Run unit tests (Vitest)
docker compose exec agent-server npm run lint # ESLint
docker compose exec agent-server npm run check-types # TypeScript type check
docker compose exec agent-server npm run build # Compile to dist/

# Watch mode (useful during development)
docker compose exec agent-server npm run test:watch

Production build

Without the dev override, changes take effect via an image rebuild:

docker compose up -d --build agent-server

The Dockerfile runs tsc at build time; the runtime layer executes node dist/server.js. No devDependencies in the runtime layer.

Testing

  • Framework: Vitest with Node environment.
  • Test files: tests/**/*.test.ts.
  • Coverage: npm run test -- --coverage (v8 provider).

Linting

  • ESLint with standard-with-typescript base config.
  • Max 300 lines per file.
  • Strict TypeScript rules (explicit return types, consistent type imports).

PHP backend

Commands

# Unit tests
docker compose exec php vendor/bin/phpunit -c dev/pimcore/pimcore-agent-bundle/phpunit.xml.dist

# Static analysis (level 6)
docker compose exec php vendor/bin/phpstan analyse -c dev/pimcore/pimcore-agent-bundle/phpstan.neon

# Code style
docker compose exec php vendor/bin/php-cs-fixer fix dev/pimcore/pimcore-agent-bundle/src/

# Areabrick / document template tooling (see Extending → Areabrick Examples)
docker compose exec php bin/console pimcore-agent:document-schema:names --brick=<brick-id>
docker compose exec php bin/console pimcore-agent:document-schema:lint

# MCP tool registry debugging
docker compose exec php bin/console pimcore-agent:mcp:list-tools # all groups + tools
docker compose exec php bin/console pimcore-agent:mcp:list-tools <group> # single group
docker compose exec php bin/console pimcore-agent:mcp:list-tools -v # include class::method

Testing

  • Framework: PHPUnit 12.5+.
  • Test directory: tests/.
  • Bootstrap: vendor/autoload.php from project root.
  • Key test areas: MCP tool registry, compiler pass, server factories, individual tools.

Static analysis

PHPStan level 6 with src/ as the analysis path. Some baseline ignores for iterables and generics.

Code style

PHP-CS-Fixer with PSR-1 / PSR-2 plus Pimcore conventions (short arrays, trailing commas, ordered imports).

Frontend assets

Working directory: assets/.

cd dev/pimcore/pimcore-agent-bundle/assets

npm run dev # Development build (RSBuild)
npm run dev-server # HMR dev server on port 3033
npm run build # Production build
npm run build-api-client # Regenerate RTK Query slices from PHP OpenAPI
npm run lint # ESLint (React + TypeScript)
npm run check-types # TypeScript type check
npm test # Unit tests (Vitest + jsdom)

Build

RSBuild with Module Federation — the bundle registers as a remote plugin consumed by the Studio UI host. Output lands under public/build/<uuid>/. Old build directories are cleaned up automatically.

API client codegen

Slices that talk to PHP endpoints (agent-config-api-slice.gen.ts, skill-config-api-slice.gen.ts) are generated from the Studio Backend OpenAPI spec via @rtk-query/codegen-openapi — same pattern as studio-ui-bundle, copilot-bundle, etc. Source of truth is the OpenApi\Attributes\* annotations on the PHP controllers under src/Controller/Studio/.

Workflow when changing PHP request/response schemas:

# 1. Update the PHP controller / Schema class with new OpenApi attributes
# 2. Clear Symfony cache so swagger-php picks up the change
docker compose exec php bin/console cache:clear --no-warmup
# 3. Refetch the spec (overwrites build/api/docs.jsonopenapi.json)
curl -s -o build/api/docs.jsonopenapi.json http://localhost/pimcore-studio/api/docs/json
# 4. Regenerate the slices (also strips broken external $refs from the global spec)
npm run build-api-client

The agent-server slice (agent-api-slice.ts) stays hand-rolled — it talks to the Node Fastify backend and uses streaming SSE endpoints that RTK Query does not model.

Testing

  • Framework: Vitest with jsdom environment.
  • Test files: js/test/**/*.test.{ts,tsx}.
  • React Testing Library for component tests.

Linting

ESLint with standard-with-typescript and React-specific rules (jsx-a11y, strict JSX formatting).

Debugging

Agent-server logs

docker compose logs -f agent-server

Structured JSON logging, configurable via AGENT_SERVER_LOG_LEVEL in .env.local:

LevelWhat is logged
errorTask failures, onComplete callback errors, unhandled exceptions.
warnModel validation warnings, non-critical issues.
info (default)Task lifecycle (started / finished / cancelled), agent switching, session creation.
debugHTTP request entry points (session ID, user, message length), stream reconnect, TaskRunner subscriptions, SSE stream open / close.
verboseEvery individual event from the TaskRunner (event type, seq number, text chunk length).

Enable detailed per-event logging:

# .env.local
AGENT_SERVER_LOG_LEVEL=verbose

Restart the agent-server container afterwards. env_file is only read at container start, so even with the dev override in place env-var changes do not hot-reload: docker compose restart agent-server.

PHP MCP debugging

Enable Symfony's debug toolbar or check var/log/dev.log for MCP request / response details. Each MCP tool logs errors via the injected LoggerInterface.

Swagger UI

Interactive API documentation at /agent-server/api/docsonly when NODE_ENVproduction. The development Docker setup sets NODE_ENV: development, so Swagger UI is enabled automatically:

http://localhost/agent-server/api/docs

In production deployments the docs route is not registered.

Common commands at a glance

IntentCommand
Reload agent configscurl -X POST -H "Authorization: Bearer $AGENT_SERVER_ADMIN_TOKEN" http://localhost/agent-server/api/admin/reload-agents
List available LLM modelscurl -H "Authorization: Bearer $AGENT_SERVER_ADMIN_TOKEN" http://localhost/agent-server/api/admin/models
Wipe Copilot CLI staterm -rf var/tmp/copilot-state/* then restart the container
Wipe user upload directoriesrm -rf var/tmp/agent-uploads/*
Inspect registered MCP tool pathsdocker compose exec php bin/console debug:container --parameter=pimcore_agent.agents.paths --format=json
List MCP tool groups and toolsdocker compose exec php bin/console pimcore-agent:mcp:list-tools

For expected behaviours when things go wrong, see Troubleshooting.