Engineering conventions
AGENTS.md at the repository root is the authoritative rule set for both humans
and AI coding agents. This page restates it in documentation form; where the two
disagree, AGENTS.md wins.
Hard rules
|
Import conventions
Each unit has its own import root. Crossing them at runtime is prohibited.
agentcore/-
Absolute
agentcore.*imports.PYTHONPATH=/appis set in the container.from agentcore.types import AgentResult, RunOptions # correct from agentcore.constants import MEMORY_ID # correct from src.types import AgentResult # WRONG — src does not exist lambdas/<function>/-
Each function directory is its own package root, so imports are top-level:
from models.seller import SellerRecord from events.base import BaseEventHandler from utils.constants import INT32_MAX libs/-
Each library uses its own package prefix:
from morpheus_llm.providers import ClaudeAgentService from morpheus_llm.tools import create_hubspot_mcp, create_tavily_mcp from morpheus_data_layer.repositories import SellerRepository, ContactRepository from morpheus_data_layer.session import get_session from morpheus_common.sentry import configure as configure_sentry from morpheus_common.events import publish_event from pyb_hubspot import upsert_company, upsert_contact
Package managers
| Context | Tool | Command |
|---|---|---|
|
|
|
|
|
|
Lambda deploy manifests |
|
|
|
|
|
Logging
-
logger = logging.getLogger(name). Neverprint(). -
Structured
key=valuefields inside the message string:"sellerId=%r name=%r". -
exc_info=Trueon every caught exception. -
logger.debug()for per-record detail that should be off in production. -
Never log payloads or secrets.
-
End multi-row handlers with a summary line —
"… completed updated=%d skipped=%d total=%d"— so a run can be assessed from one line.
Pydantic
Pydantic v2 throughout.
-
model_config = ConfigDict(extra="ignore")for EventBridge envelopes that use only some fields. -
model_config = ConfigDict(str_strip_whitespace=True, frozen=True)for immutable domain models such asSellerRecord. -
field_validator/model_validator(v2), never the v1@validator. -
model_dump_json()for serialisation, neverjson.dumps(model.dict()). -
Cross-field requirements belong in a
model_validator(mode="after")so an invalid message is rejected at the boundary. Example:ContactUnsubscribeEventrequires at least one ofhubspot_contact_idorcontact_email.
Data layer
-
Attribute names are camelCase and match the database column exactly — no
name=override inmapped_column. -
ForeignKey("subscriptions.id")is not declared in the ORM; that table is not managed here and the constraint is enforced at the database level only. -
One model per file in
morpheus_data_layer/models/, exported frommodels/init.py. -
Repositories are stateless and instantiated once at module level:
_repo = SellerRepository(). -
Repositories never open sessions and never commit. The caller owns the transaction:
with get_session() as session:. -
Every new repository is exported from
repositories/init.py. -
Use
upsert()withconflict_columnsfor idempotent writes, naming columns in camelCase.
See Data.
Event handlers
Adding an event type to a Lambda is always the same four steps:
-
Create
events/<name>.pywith a handler extending that function’s base class (BaseEventHandler,BaseEnrichmentHandler,BaseEmailEventHandler,BasePhoneEventHandler). -
Define or reuse a Pydantic model for the payload.
-
Register the routing key in
registry.py. -
Add the trigger to
lambdas/template.yaml— an SQSEventsentry, or aSchedulewith anInputwhosedetail-typematches the registry key exactly.
Never add routing logic to handler.py. Full walkthrough:
Adding a new event.
CDK
-
One stack:
AgentCoreStackincdk/lib/stacks/agentcore-stack.ts. -
Never import an external role with
fromRoleArn(). -
No business logic — providers, models, and prompts live in runtime code.
-
Docker build context is
agentcore/; platform targetlinux/arm64. -
pnpmonly.
SAM
-
All functions and layers live in the single
lambdas/template.yaml. -
Layers are built from
libs/withBuildMethod: python3.12. -
Every handler calls
configure_sentry()at cold start, beforeload_secrets(). -
Credentials come from
AgentcoreSecret— never from environment variables. -
SQS triggers declare
ReportBatchItemFailures. -
Scheduled triggers set the routing key with
Input: '{"detail-type":"…"}'. -
Layer order in every function:
Layers: - !Ref MorpheusCommonLayer # always first — Sentry - !Ref MorpheusDataLayer # if DB access is needed - !Ref MorpheusLlmLayer # if an AI agent is needed
morpheus-llm
-
New tools go in
morpheus_llm/tools/builtin/<name>.py, use the@tooldecorator fromstrands(docstring is the description, annotations are the schema), and are exported fromtools/init.py. -
MCP tools are factory functions taking credentials as explicit keyword arguments — never read from
os.environinside the library. -
Model ids always come from
morpheus_llm.utils.AgentModelIds. Do not strip theus.prefix; it selects the cross-region inference profile. -
For structured results, set
AgentConfig.structured_output_modeland readresult.structured_output. Never parse JSON out ofresult.output.
Common mistakes
| Mistake | Correct approach |
|---|---|
|
|
|
|
|
|
|
|
Reading secrets inside a library tool factory |
Accept credentials as keyword arguments; the caller fetches them |
|
|
Standalone helper functions for shared Lambda logic |
A shared base class with inherited methods |
Referencing |
Those columns no longer exist — see |
|
Omit it; enforced at the database level |
Hardcoding |
Reference SSM Parameter Store or Secrets Manager |
Adding a dependency without re-exporting |
|