Runtime architecture
Component map
flowchart TB
subgraph container ["AgentCore container (linux/arm64, PYTHONPATH=/app)"]
Main["main.py\n@app.entrypoint agent_handler()"]
Cfg["config/loader.py\nload_config() → AppConfig"]
Reg["workflows/registry.py\nWorkflowRegistry"]
Base["workflows/base_workflow.py\nBaseWorkflow.execute_steps()"]
subgraph wf ["workflows/builtin/"]
PW["PromptWorkflow\n'test'"]
DW["DiallerAgentWorkflow\n'dialler-agent'"]
end
subgraph ag ["providers/agents/"]
DA["dialler_agent\nClaudeAgentService singleton"]
end
Types["types/__init__.py\nWorkflowStep / WorkflowResult"]
end
subgraph lib ["morpheus-llm (installed dependency)"]
CS["ClaudeAgentService"]
BS["BaseAgentService\ntools + memory"]
Tools["create_hubspot_mcp()\ncreate_tavily_mcp()\nadd_numbers"]
end
SM(["Secrets Manager"])
BR(["Amazon Bedrock\nClaude Sonnet 4"])
MEM[("AgentCore Memory")]
HS(["HubSpot MCP"])
Main --> Cfg --> SM
Main --> Reg --> PW & DW
DW --> Base
PW & DW --> DA --> CS --> BS
CS --> BR
BS --> MEM
DA --> Tools --> HS
Base --> Types
Module responsibilities
main.py-
Startup and routing. Calls
load_config(), thenbuild_registry(), then startsBedrockAgentCoreApp. The@app.entrypointasync generator parses the payload, buildsRunOptionsfrom the AgentCore context, delegates toregistry.run(), and yields one SSE event. config/loader.py-
Owns
load_config()— the single source of credentials. ReadsSECRET_ARN, fetches from Secrets Manager, validates with_SecretsSchema, injects intoos.environ, builds an immutableAppConfig, and caches it in a module global. See Configuration. workflows/registry.py-
WorkflowRegistry— adict[str, WorkflowDefinition]withregister(),list(), andrun(). Also defines theWorkflowDefinitionruntime-checkable protocol. workflows/base_workflow.py-
The multi-step execution engine:
add_step(),execute_steps(), and{{variable}}prompt templating. One shared StrandsAgentper invocation, so steps accumulate conversation history automatically. workflows/builtin/-
Concrete workflows.
PromptWorkflowimplements the protocol directly;DiallerAgentWorkflowextendsBaseWorkflow. providers/-
Thin re-exports of
morpheus_llm.providers(BaseAgentService,ClaudeAgentService), plusproviders/agents/holding module-level agent singletons. types/init.py-
WorkflowStep,WorkflowStepResult,WorkflowResult,InvocationPayload, and re-exports ofAgentConfig,AgentResult,RunOptionsfrommorpheus_llm. tools/builtin/-
Local
@toolfunctions. Production tools live inmorpheus_llmso the Lambdas can use them too. constants.py-
Environment-driven constants:
LOG_GROUP_NAME,AGENT_MAX_TOKEN,MEMORY_ID,DEFAULT_SYSTEM_PROMPT,DEFAULT_AGENT_NAME.
Startup sequence
sequenceDiagram participant P as Python import participant C as load_config() participant SM as Secrets Manager participant A as dialler_agent module participant R as build_registry() participant S as BedrockAgentCoreApp P->>C: load_config() C->>SM: GetSecretValue(SECRET_ARN) SM-->>C: JSON credentials C->>C: _SecretsSchema.model_validate() C->>C: inject into os.environ, cache AppConfig P->>R: build_registry() R->>A: import agentcore.providers.agents.dialler_agent A->>C: load_config() (cache hit) A->>A: construct ClaudeAgentService singleton R->>R: register PromptWorkflow + DiallerAgentWorkflow P->>S: BedrockAgentCoreApp() then app.run()
Order matters. dialler_agent calls load_config() at module import time to get
HubSpot credentials for its tool factory. Because main.py calls load_config()
before importing the registry, the agent module hits a warm cache. A missing
SECRET_ARN therefore fails at startup with a clear RuntimeError rather than at
the first invocation.
Request lifecycle
sequenceDiagram
participant C as Caller
participant EP as agent_handler()
participant R as WorkflowRegistry
participant W as DiallerAgentWorkflow
participant B as BaseWorkflow
participant S as ClaudeAgentService
participant A as strands.Agent
participant M as AgentCore Memory
participant BR as Bedrock
participant H as HubSpot MCP
C->>EP: {"workflow":"dialler-agent","payload":{…},"actorId":"…"}
EP->>EP: RunOptions(session_id=ctx.session_id, actor_id=payload.actorId)
EP->>R: run("dialler-agent", payload, options)
R->>W: run(payload, options)
W->>B: execute_steps({"phoneNumber": …}, options)
B->>S: new_session(options)
S->>A: Agent(model, system_prompt, tools(), session_manager)
B->>A: step 1 "analyse-history" prompt + JSON schema
A->>M: retrieve memory records
A->>BR: inference
BR-->>A: tool_use
A->>H: HubSpot MCP call
H-->>A: contact + history
A-->>B: text → validated AnalyseHistoryOutput
B->>A: step 2 "prepare-brief" (same Agent, history intact)
A->>BR: inference
A->>M: persist turns
A-->>B: text → validated PrepareBriefOutput
B-->>W: WorkflowResult
W-->>EP: WorkflowResult
EP-->>C: SSE event {"session_id": …, "data": WorkflowResult}
Two design decisions worth knowing
Why an async generator entrypoint
The /invocations route uses SSE streaming. Yielding from the entrypoint routes
into the SSE-aware code path, which manages heartbeats correctly for long-running
workflows. A plain return would take the non-streaming path and risk a client
timeout on a multi-step workflow.
Why a fresh Agent per invocation
ClaudeAgentService instances are module-level singletons living for the container’s
lifetime. Constructing the Strands Agent in init would accumulate
agent.messages across every request the container ever served, leaking one caller’s
conversation into another’s. _make_agent() is therefore called per invocation, from
run() or new_session().
The same reasoning applies to MCP tools: MCPClient transports hold an open HTTP
session, so tools are supplied as a factory lambda and re-created per invocation.
Why python -m agentcore.main
Running python agentcore/main.py puts agentcore/ at sys.path[0], which makes
agentcore/types/ shadow the standard-library types module and breaks imports
inside the SDKs. The -m form puts the working directory (/app) first, so
agentcore.types resolves as a package attribute and the stdlib module stays
reachable. The dev Make target uses python -m agentcore.main.
|
The |