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

NEVER use pip or poetry for agentcore/ or libs/ — always use uv
NEVER import between agentcore/, lambdas/, and libs/ at runtime
NEVER use src.* import paths — they do not exist in this repo
NEVER split the CDK into more than one stack (AgentCoreStack only)
NEVER use iam.Role.fromRoleArn() in CDK — CDK owns all IAM
NEVER put agent config (model ID, prompt, provider) in CDK or SAM env vars
NEVER commit .env files or any file containing real API keys or secrets
NEVER log full invocation payloads — they may contain PII
NEVER use npm install in cdk/ — use pnpm
NEVER run python agentcore/main.py — always python -m agentcore.main
NEVER create a new CDK stack — everything goes in AgentCoreStack

Import conventions

Each unit has its own import root. Crossing them at runtime is prohibited.

agentcore/

Absolute agentcore.* imports. PYTHONPATH=/app is 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

agentcore/ dependencies

uv

uv sync, uv add <pkg>

libs/ dependencies

uv

uv add <pkg> in the library directory

Lambda deploy manifests

uv export

uv export --no-dev --no-hashes -o requirements.txt

cdk/ dependencies

pnpm

pnpm install, pnpm add <pkg>

Logging

  • logger = logging.getLogger(name). Never print().

  • Structured key=value fields inside the message string: "sellerId=%r name=%r".

  • exc_info=True on 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 as SellerRecord.

  • field_validator / model_validator (v2), never the v1 @validator.

  • model_dump_json() for serialisation, never json.dumps(model.dict()).

  • Cross-field requirements belong in a model_validator(mode="after") so an invalid message is rejected at the boundary. Example: ContactUnsubscribeEvent requires at least one of hubspot_contact_id or contact_email.

Data layer

  • Attribute names are camelCase and match the database column exactly — no name= override in mapped_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 from models/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() with conflict_columns for idempotent writes, naming columns in camelCase.

See Data.

Event handlers

Adding an event type to a Lambda is always the same four steps:

  1. Create events/<name>.py with a handler extending that function’s base class (BaseEventHandler, BaseEnrichmentHandler, BaseEmailEventHandler, BasePhoneEventHandler).

  2. Define or reuse a Pydantic model for the payload.

  3. Register the routing key in registry.py.

  4. Add the trigger to lambdas/template.yaml — an SQS Events entry, or a Schedule with an Input whose detail-type matches the registry key exactly.

Never add routing logic to handler.py. Full walkthrough: Adding a new event.

Best-effort rule

In the enrichment pipeline, the database write commits first; the AI enrichment and scoring steps run afterwards inside try/except. Those steps must never cause an SQS retry — catch everything, log at ERROR, return.

CDK

  • One stack: AgentCoreStack in cdk/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 target linux/arm64.

  • pnpm only.

SAM

  • All functions and layers live in the single lambdas/template.yaml.

  • Layers are built from libs/ with BuildMethod: python3.12.

  • Every handler calls configure_sentry() at cold start, before load_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 @tool decorator from strands (docstring is the description, annotations are the schema), and are exported from tools/init.py.

  • MCP tools are factory functions taking credentials as explicit keyword arguments — never read from os.environ inside the library.

  • Model ids always come from morpheus_llm.utils.AgentModelIds. Do not strip the us. prefix; it selects the cross-region inference profile.

  • For structured results, set AgentConfig.structured_output_model and read result.structured_output. Never parse JSON out of result.output.

Common mistakes

Mistake Correct approach

from src.types import …

from agentcore.types import …

pip install <pkg> in agentcore/

uv add <pkg>

npm install in cdk/

pnpm install

python agentcore/main.py

python -m agentcore.main

Reading secrets inside a library tool factory

Accept credentials as keyword arguments; the caller fetches them

json.loads(result.output) for structured output

result.structured_output

Standalone helper functions for shared Lambda logic

A shared base class with inherited methods

Referencing confirmed_email or hubspotContactId on Seller

Those columns no longer exist — see models/seller.py

ForeignKey("subscriptions.id") in an ORM model

Omit it; enforced at the database level

Hardcoding SENTRY_DSN in template.yaml

Reference SSM Parameter Store or Secrets Manager

Adding a dependency without re-exporting requirements.txt

make deploy-<fn> in lambdas/, which runs uv export first