Project layout and placeholders
Every file the creator generates, what each one does, and how each placeholder is filled.
The creator writes a new directory, by default azurefoundry_<short>_skills/, next to the source skill in your repository. The skill folder stays where it is; the Dockerfile copies it into the image at build time. That is why every build runs from the repository root.
<repo-root>/
├── skills/report-generator/ # the source skill (unchanged)
│ ├── SKILL.md
│ └── scripts/
└── azurefoundry_report_generator_skills/
├── __main__.py # entry point: health probe, run_turn, uvicorn
├── claude_skills_agent.py # the SDK core: options, prompt, events, tokens
├── claude_skills_agent_executor.py # shared agent instance + per-context session map
├── http_agent_server.py # transport: /health, /execute, /tasks/{id}/cancel
├── Dockerfile
├── .dockerignore
├── .env.example
├── pyproject.toml # what the image installs from
├── requirements.txt # kept in step with pyproject.toml
├── README.md
└── tests/ # local diagnostics, never shipped in the image
├── README.md
├── test_run.py
├── test_foundry_direct.py
├── test_anthropic_sdk_foundry.py
├── inspect_foundry_client.py
└── debug_sdk.pyFor the AMC variant, keep claude_skills_agent.py and the build files, skip http_agent_server.py, claude_skills_agent_executor.py, and the HTTP parts of __main__.py, and add app.py. See Serve the agent to AMC with amc_sdk.
What each file does
| File | Kind | Job |
|---|---|---|
claude_skills_agent.py | Templated | Builds ClaudeAgentOptions, wraps the user message in the prompt strategy, runs query(), translates SDK messages into wire events, and reads tokens and cost off ResultMessage. |
claude_skills_agent_executor.py | Templated | Owns one shared agent instance behind a lock, initializes it at startup, and maps each context_id to a session id. |
__main__.py | Templated | Loads .env, defines the health probe, maps agent strings to NDJSON events in _run_turn, and starts uvicorn. |
http_agent_server.py | Verbatim | The plain-HTTP transport: request validation, NDJSON streaming, concurrency cap, and cancellation. |
Dockerfile | Templated | python:3.12-slim image with the skill baked in at /app/.claude/skills/<skill-name>/, running as a non-root user. |
.dockerignore | Verbatim | Keeps .env, tests/, .git/, virtualenvs, and caches out of the image. |
pyproject.toml | Templated | The dependency list the image installs (uv pip install --system -e .). |
requirements.txt | Templated | The same dependencies for local installs. The image never reads it. |
.env.example | Templated | Every environment variable the agent reads, with defaults. Copy to .env. |
README.md | Templated | Build, local smoke test, and troubleshooting for this agent. |
tests/test_run.py | Templated | Phased local smoke test: env, SDK auth, skill discovery, adapter wiring. |
tests/*.py (other four), tests/README.md | Verbatim | Credential and SDK diagnostics. See Test the agent locally. |
Simple placeholders
These are plain search-and-replace across every .tpl file. The examples are for a skill whose frontmatter says name: report-generator.
| Placeholder | What it is | Example |
|---|---|---|
{{AGENT_DIR}} | New agent directory | azurefoundry_report_generator_skills |
{{AGENT_CLASS}} | Agent class name | ReportGeneratorSkillsAgent |
{{EXECUTOR_CLASS}} | Executor class name | ReportGeneratorSkillsExecutor |
{{CREATE_EXECUTOR_FN}} | Executor factory function | create_report_generator_executor |
{{INIT_FN}} | Startup-init function | initialize_report_generator_agents_at_startup |
{{SKILL_NAME}} | The skill's frontmatter name:, exactly | report-generator |
{{SKILL_SOURCE_PATH}} | Skill folder, relative to the repo root | skills/report-generator |
{{AGENT_NAME_HUMAN}} | Display name | Report Generator Agent |
{{AGENT_DESCRIPTION}} | One-line description | Builds a weekly report with the report-generator skill. |
{{PORT}} | TCP port for the plain-HTTP server | 9024 |
{{PACKAGE_NAME}} | pyproject.toml name | claude-skills-report-generator-agent |
{{PACKAGE_DESCRIPTION}} | pyproject.toml description | Report generator agent powered by claude-agent-sdk |
{{AGENT_DOCKER_TAG}} | Image tag prefix and container app name | report-generator-skills-agent |
{{SKILL_DISPLAY_NAME}} | Skill entry name in the README's registry snippet | Report Generator |
{{SKILL_DESCRIPTION_SHORT}} | Skill entry description | Weekly report rendered as HTML. |
{{SKILL_ID_SNAKE}} | Skill entry id | report_generator |
{{SKILL_TAGS}} | Comma-separated tags | reports, claude, skill |
Naming rules
| Identifier | Rule | Example |
|---|---|---|
Skill name: | kebab-case, used in full wherever {{SKILL_NAME}} appears | report-generator |
{{AGENT_DIR}} | azurefoundry_ + snake_case + _skills | azurefoundry_report_generator_skills |
{{AGENT_CLASS}} | PascalCase, no underscores, suffix SkillsAgent | ReportGeneratorSkillsAgent |
{{PACKAGE_NAME}} | claude-skills- + kebab-case + -agent | claude-skills-report-generator-agent |
{{AGENT_DOCKER_TAG}} | short kebab-case + -skills-agent | report-generator-skills-agent |
| Container app name | <short>-skills-agent-uami | report-generator-skills-agent-uami |
If the derived names get unwieldy, strip filler words from the agent-side identifiers only. {{SKILL_NAME}} always keeps the skill's full name, because the SDK matches it.
Block markers
Block markers are replaced with code rather than a single value. Each sits next to a comment in the template that explains the choice.
| Marker | File | Replace with |
|---|---|---|
{{OPTIONAL_OUTPUTS_DIR_CONST}} | claude_skills_agent.py | OUTPUTS_DIR = os.getenv("REPORT_OUTPUTS_DIR", "/outputs") if the skill writes files, else remove |
{{OPTIONAL_MAKEDIRS}} | claude_skills_agent.py, in create_agent() | os.makedirs(OUTPUTS_DIR, exist_ok=True), or remove |
{{SKILL_ENV_VARS}} | claude_skills_agent.py, in ClaudeAgentOptions(env=...) | Env vars the skill's scripts read at runtime, or nothing |
{{PROMPT_STRATEGY_BODY}} | claude_skills_agent.py, _build_prompt() | Pattern A or B from the prompt strategies |
{{TOOL_SCRIPT_LABELS}} | claude_skills_agent.py, _describe_tool() | A map of scripts/*.py filenames to short progress labels |
{{OPTIONAL_OUTPUTS_HEALTH_CHECK}} | __main__.py, _health() | An outputs-dir writability check, or remove |
{{OPTIONAL_OUTPUTS_MISSING_CHECK}} | __main__.py, _health() | The matching missing.append(...) line, or remove |
{{DOCKERFILE_SKILL_PREP_BLOCK}} | Dockerfile | One or more filesystem prep patterns, concatenated |
{{OPTIONAL_OUTPUTS_ENV}} | Dockerfile | ENV REPORT_OUTPUTS_DIR=/outputs, or remove |
{{SKILL_EXTRA_DEPS}} | pyproject.toml and requirements.txt | Only the packages the skill's scripts import, one per line |
{{AGENT_TURN_DESCRIPTION}} | README.md | What one run does, end to end |
{{PREREQUISITES_BLOCK}} | README.md | Volume mounts, network access, external accounts |
{{ADDITIONAL_VOLUMES_BLOCK}} | README.md, in the docker run example | -v "${PWD}/outputs:/outputs" and similar, or empty |
A few more markers appear in the templates: {{NONROOT_EXTRA_DIRS}} (extra directories the non-root user must own, in the Dockerfile), {{OUTPUTS_ENV_VAR}} and {{OPTIONAL_OUTPUTS_ENV_VAR}} (the outputs variable's name, in the agent and .env.example), {{OPTIONAL_OUTPUTS_ENV_SETUP}} (pointing that variable at a temp dir in tests/test_run.py), and {{PROMPT_DOCSTRING}} (the _build_prompt() docstring).
Related
Last updated 2026-10-21