- name
- atlas-agent-engine
- description
- Guide for extending Atlas Agent Engine templates and generated apps. Use this before editing templates/hello-world-agent, templates/insurance-agent, templates/chatbot-client, or any generated project with agent.yaml, agent_engine_sdk_langgraph, agent_engine_runner_shared, @app.tool, memory, guardrails, human review, or Atlas Agent Engine API streaming. Covers project layouts, template-specific files, tools, graph flow, memory, HITL, guardrails, testing, and deployment.
# Atlas Agent Engine Agent Template Guide
Use this skill when building from, maintaining, or extending an Atlas Agent Engine sample template or a project generated from one. For setup and quickstart, still read the template's `README.md` first.
## Start Here
Identify which template you are editing before changing files:
| Template | Type | What it demonstrates | Main files |
|---|---|---|---|
| `templates/hello-world-agent/` | Python Agent Engine agent | Minimal LangGraph assistant, date tool, optional profile memory, provider selection | `src/hello_world_agent/main.py`, `llm.py`, `tools.py`, `system_message.py`, `state.py` |
| `templates/insurance-agent/` | Python Agent Engine agent | Quotes, policies, claims, MongoDB storage, memory, guardrails, human review | `src/insurance_agent/main.py`, `policy_store.py` |
| `templates/chatbot-client/` | Next.js client app | Chat UI that streams to an existing Atlas Agent Engine workspace | `app/api/chat/route.ts`, `app/page.tsx`, `components/chat/*` |
The `agents/` directory contains fuller reference apps. Use those as examples, but keep `templates/` clean, scaffold-friendly, and small enough for `agentengine create`.
## Common Rules
- Preserve the template's current layout. Do not invent `graph.py`, `prompts.py`, `store.py`, or one-file-per-tool modules unless the template already uses that pattern or the user explicitly asks for a refactor.
- Keep generated apps standalone. A copied template should work from its own root with its own `agent.yaml`, `pyproject.toml` or `package.json`, environment example, and README.
- Keep secrets in `.env` or `.env.local`; never commit filled secret files. `env.example` and `.env.example` should contain names and safe sample values only.
- Let `agent.yaml` own app identity, entrypoint, and runtime feature flags. Provider and model selection belongs in the app's own LLM module, not `agent.yaml`. Do not move feature flags into `.env`. Keep `agent.yaml` portable: local-development-only settings (service ports, the local-vs-remote MongoDB toggle) belong in `dev.yaml` next to it.
- Never edit `.agentengine/`; it is generated by the `agentengine` CLI and can be overwritten.
- Use existing helper surfaces from the SDK: `app.llm(...)`, `app.get_tools()`, `app.get_tool_schemas()`, `app.checkpointer()`, `app.memory`, and `app.validate_llm_response(...)`.
- Keep template code easy to read. Avoid chains of tiny single-use helpers unless they create a real boundary.
## Runtime Model
`agentengine dev up` starts the local platform processes needed by a Python agent:
```text
User message
-> Orchestration Engine (OE)
-> Agent Sandbox runs the LangGraph graph
-> LLM decides whether to call tools
-> Tool Sandbox runs @app.tool(is_local=False) tools
-> Agent Sandbox runs @app.tool(is_local=True) tools
-> app.validate_llm_response(...) applies guardrails when enabled
-> OE records native LangGraph interrupts and resumes them by interrupt ID
```
The local Playground UI is exposed on the port configured in `dev.yaml` under `services.playground.port`, currently `3000` in the Python templates.
## Project Layouts
### Generated Python Agent
The Python templates are intended to be copied into a new standalone project:
```text
my-agent/
├── agent.yaml
├── dev.yaml
├── env.example
├── pyproject.toml
├── pyrightconfig.json
├── README.md
├── .agents/skills/atlas-agent-engine/SKILL.md
├── .claude/skills/atlas-agent-engine/SKILL.md
└── src/
└── my_agent/
├── __init__.py
├── main.py
└── ...
```
Run `agentengine dev up`, `agentengine init`, `agentengine build`, and `agentengine deploy` from the generated app root.
### This Examples Monorepo
The examples repository root is a uv workspace and has a root `agent.yaml` with an `agents:` list. That monorepo shape is for maintaining examples; it is not required for a generated app.
Templates are intentionally outside the root uv workspace. Validate them directly under `templates/`, not as root workspace members.
## Template File Map
| Goal | Hello World | Insurance | Chatbot Client |
|---|---|---|---|
| App metadata and feature flags | `agent.yaml` | `agent.yaml` | `.env.example`, `README.md` |
| LLM provider selection | `src/hello_world_agent/llm.py` | `src/insurance_agent/llm.py` | Existing deployed workspace, not local LLM code |
| Persona and workflow rules | `src/hello_world_agent/system_message.py` | `SYSTEM_PROMPT` in `src/insurance_agent/main.py` | UI copy and request handling |
| Tool registration | `src/hello_world_agent/tools.py` via `register(app)` | `@app.tool(...)` functions in `src/insurance_agent/main.py` | Not applicable |
| Graph assembly | `build_agent()` in `main.py` | `build_agent()` in `main.py` | Not applicable |
| State shape | `src/hello_world_agent/state.py` | `AgentState` in `main.py` | React component state |
| App data storage | Optional memory only | `src/insurance_agent/policy_store.py` | Browser/client state only |
| Platform API integration | SDK runtime | SDK runtime | `app/api/chat/route.ts` |
## `agent.yaml`
For Python agents, `entrypoint` is a Python import path to the SDK `App` object. In the current src-layout templates, do not prefix the path with `src.`:
```yaml
name: hello-world-agent
entrypoint: hello_world_agent.main:app
framework: langgraph
features:
durable_workflow: true
memory: false
guardrails: false
```
Rules:
- `name` should be a lowercase app slug with hyphens.
- `entrypoint` must point to the module and variable that expose `app`.
- Provider and model are not part of `agent.yaml`. New CLI scaffolds contain one provider import, a direct constructor and `MODEL` in the LLM module. When changing providers, update the client and model; keep the shared `LLM_API_KEY` lookup.
- Generated clients contain one supported provider or Azure recipe; manual setup contains a builder stub. Keep credentials in `.env` or project secrets and preserve send-time authentication injection so traces cannot serialize keys.
- `features.memory: true` requires `VOYAGE_API_KEY`.
- `features.guardrails: true` requires a guardrails service endpoint such as `GUARDRAILS_SERVER_URL` in templates that expose that setting.
- `features.durable_workflow: true` lets the platform preserve and resume workflow state across runtime restarts.
- Keep provider support accurate per template. `hello-world-agent` and `insurance-agent` both support Anthropic, Gemini, OpenAI, and xAI.
- Keep `agent.yaml` portable across environments. Local-development-only settings (service ports, the local-vs-remote MongoDB toggle) live in `dev.yaml`, not `agent.yaml`.
## `dev.yaml`
`dev.yaml` sits next to `agent.yaml` and holds settings that only apply when you run the agent locally with `agentengine dev up`: host port overrides for the local stack and whether to use a local MongoDB Atlas container or an external cluster. It is never read by build or deploy, so it has no effect on a platform deploy even if committed. The file is optional — omit it (or any field) to get the defaults: a local Atlas container on port 27017 and Docker-assigned host ports.
```yaml
services:
playground:
port: 3000 # Pinned so the browser UI has a stable URL.
# mongodb:
# local: false # Use an external cluster; set MONGODB_URI in .env.
```
Valid service names: `oe`, `aer`, `tool`, `playground`, `memory-server`, `grpc`, `mongodb`. Every service takes a `port`; `mongodb` also takes `local` (set `local: false` plus `MONGODB_URI` in `.env` to use an external cluster). Consult the installed SDK documentation for the full schema.
## Python Dependencies
Preserve each template's current dependency style unless the change truly requires a new package. Use `uv add <package>` inside a generated app when adding dependencies. In this repo's templates, edit `pyproject.toml` carefully and keep the agent-engine SDK packages pinned to an exact published version.
## Tools
Use the existing tool registration style for the template.
Hello World:
```python
from datetime import datetime
from zoneinfo import ZoneInfo
def register(app: App) -> None:
@app.tool(is_local=False)
def get_current_date(timezone_name: str = "UTC") -> str:
"""Return today's date in ISO 8601 format for an IANA timezone."""
timezone = ZoneInfo(timezone_name)
return json.dumps({"date": datetime.now(timezone).date().isoformat()})
```
Insurance:
```python
@app.tool(is_local=False)
def lookup_policy(policy_number: str) -> str:
"""Look up an insurance policy by policy number."""
policy = policy_store.get_policy(policy_number)
if policy:
return json.dumps(policy, indent=2)
return json.dumps({"error": f"No policy found: {policy_number}"})
```
Rules:
- Every tool must have a clear docstring because the LLM uses it to decide when to call the tool.
- Return a string, normally `json.dumps(...)`, so the LLM receives structured content.
- Use `app.get_current_user_id()` to scope user data. Never hardcode user IDs.
- After adding or changing a tool, update the system prompt so the LLM knows when and how to use it.
- Use `redact_fields=[...]` for sensitive tool arguments that should not appear in logs.
- Prefer `is_local=False` for deterministic app tools, database reads/writes, and external APIs that should run in the isolated Tool Sandbox.
- Use `is_local=True` when a tool must run in the Agent Sandbox process, especially human review suspension or runtime-local side effects.
## Human Review
For durable LangGraph agents, place the review tool in the agent sandbox and call
`interrupt()` with replay-stable payload content. Include a response schema when the
Playground or API client should render and validate structured reviewer input:
```yaml
sandboxes:
agent:
tools:
- human_review
tool:
tools: []
```
```python
import json
from langgraph.types import interrupt
REVIEW_RESPONSE_SCHEMA = {
"type": "object",
"required": ["decision"],
"properties": {
"decision": {"type": "string", "enum": ["approved", "denied"]},
"reviewer_notes": {"type": "string"},
},
"additionalProperties": False,
}
@app.tool()
def human_review(claim_id: str, reason: str) -> str:
"""Request human review before resolving a claim."""
answer = interrupt(
{
"action": "review_claim",
"task_id": f"REVIEW-{claim_id}",
"claim_id": claim_id,
"reason": reason,
"response_schema": REVIEW_RESPONSE_SCHEMA,
}
)
if not isinstance(answer, dict):
raise TypeError("human review resume value must be an object")
return json.dumps(answer)
```
Build interrupt identity and payload fields from stable workflow inputs. Random IDs,
timestamps, and fresh external reads inside the payload can change during replay.
Continue a structured interrupt through the same session using the interrupt ID
reported in `suspend_context.interrupts[].id`:
```json
{
"session_id": "existing-session-id",
"resume_map": {
"interrupt-id": {
"decision": "approved",
"reviewer_notes": "Verified supporting documents."
}
}
}
```
For the insurance template, keep the prompt's post-review contract intact: after
either an approved or denied review returns, call `resolve_claim` with the decision
and reviewer notes, then notify the customer.
## Memory
Memory is optional and scoped by `user_id`. Enable it with
`features.memory: true` in `agent.yaml`.
Memory return shapes differ by SDK revision: some return plain values
(`build_context` a `str`, writes a `bool` or an id), others return typed results
(`ContextResponse.formatted_context`, `Create*Result.acknowledged`). Whether a
read accepts `visibility=` varies separately from the return shape, so check both
against the revision your agent resolves — see
[migrate-app-memory](../migrate-app-memory/SKILL.md) for the matrix.
The snippets below read results defensively, which is what the examples in this
repo do and what works on either surface. Note that a typed wrapper is truthy
even when it carries no context, so unwrap before testing for content.
Semantic memory:
```python
user_id = app.get_current_user_id()
result = app.memory.save_semantic(
text="Customer prefers comprehensive coverage",
label=f"{user_id}_coverage_preference",
source="insurance_agent",
metadata={"type": "customer_info", "info_key": "coverage_preference"},
user_id=user_id,
)
if getattr(result, "acknowledged", result):
... # the write landed
context = app.memory.build_context(query="coverage preference", user_id=user_id)
memory_text = getattr(context, "formatted_context", context)
memory_text = memory_text if isinstance(memory_text, str) else ""
```
Episodic memory:
```python
episode = app.memory.save_episode(
title="Policy created for customer",
content="Customer purchased comprehensive auto policy.",
summary="Policy created with comprehensive coverage.",
participants=["Customer", "Agent"],
user_id=user_id,
)
if getattr(episode, "acknowledged", bool(episode)):
episode_id = getattr(episode, "id", episode)
```
Taxonomic memory is used by the insurance template for domain knowledge like insurance terms and coverage options.
When memory is disabled, tools should fail gracefully and explain that memory is not enabled. Enable memory with `features.memory: true` and `VOYAGE_API_KEY`.
## Guardrails
`app.validate_llm_response(response)` is the application hook. The SDK treats it as active only when guardrails are enabled/configured.
For templates with guardrail support:
- Set `features.guardrails: true` in `agent.yaml`.
- Set the matching guardrails endpoint in `.env`.
- Keep validation close to the LLM response in the graph node, before returning the message to the user.
## Graph Changes
Both Python templates use LangGraph and a `ToolNode` loop:
```python
builder = StateGraph(AgentState)
builder.add_node("agent", agent_node)
builder.add_node("tools", ToolNode(tools))
builder.add_edge(START, "agent")
builder.add_conditional_edges("agent", should_continue, {"tools": "tools", "end": END})
builder.add_edge("tools", "agent")
graph = builder.compile(checkpointer=app.checkpointer())
```
Rules:
- Keep state JSON-serializable: strings, numbers, booleans, lists, dicts, datetimes, and LangChain message objects.
- Do not put database clients, file handles, closures, thread objects, or other runtime resources in graph state.
- Use `add_messages` for message history.
- Keep the user-facing response rule explicit when tool results are not visible to the user.
## Chatbot Client Template
`templates/chatbot-client` is not a Python agent and should not import `agent_engine_sdk_langgraph`.
When editing it:
- Use `pnpm`, not uv.
- Preserve the Next.js/Vercel AI SDK streaming shape in `app/api/chat/route.ts`.
- Keep workspace/API values in `.env.local`, copied from `.env.example`.
- Preserve any existing Next.js version warning in the template: inspect local Next docs under `node_modules/next/dist/docs/` before changing framework APIs.
## Validation
For Python template changes in this repo:
Voir sur GitHub