Skip to content

Commit 0a1574a

Browse files
authored
Add container deployment support for AgentCore Runtime (#334)
* Add container agent support - Add Container as a build type for agents (create, add, dev, deploy, invoke, package) - Add Dockerfile and dockerignore templates for Python container agents - Add container dev server with Docker build, run, volume mount, and hot-reload - Add container packaging with Docker runtime detection and validation - Add Docker prerequisite check and runtime detection utility - Wire userId (default: "default-user") through invoke flow for container auth - Log userId in invoke request logs - Upgrade vended @aws/agentcore-cdk to ^0.1.0-alpha.2 - Fix eslint require-await errors in codezip-dev-server and container packaging - Simplify BaseRenderer to use copyAndRenderDir for container templates * Fix CI: add missing buildType in test, revert CDK dep to alpha.1 - Add buildType: 'CodeZip' to schema-mapper test baseConfig (merge artifact) - Revert @aws/agentcore-cdk to ^0.1.0-alpha.1 (alpha.2 not yet published; semver ^0.1.0-alpha.1 already covers alpha.2 once available) * fix: sequential port checks to avoid bind race condition * test: add unit tests for container agent support * Address PR review feedback for container support - Add port comments to Dockerfile EXPOSE directive - Fix container runtime null check (info.runtime !== null) - Use path.join() in getDockerfilePath for cross-platform support - Use CONTAINER_RUNTIMES constant instead of hardcoded array - Change dynamic import to static import in container-dev-server - Check ports sequentially to avoid bind race conditions * fix: defer load_model() to first invocation for Container build compatibility In Container builds, the Python module loads at container startup before any request context exists. API-key-based providers use @requires_api_key which needs a workload access token only available within request context, causing ValueError at import time. Defer load_model() using lazy initialization in all affected templates: - Strands: get_or_create_agent() singleton - LangChain/LangGraph: get_or_create_model() singleton - Google ADK / OpenAI Agents: ensure_credentials_loaded() guard ---------
1 parent 8579540 commit 0a1574a

68 files changed

Lines changed: 2632 additions & 372 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,3 +58,5 @@ workdir-tmp/
5858

5959
# Bun
6060
bun.lock
61+
62+
.agentreview

src/assets/__tests__/__snapshots__/assets.snapshot.test.ts.snap

Lines changed: 45 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -356,6 +356,8 @@ exports[`Assets Directory Snapshots > File listing > should match the expected f
356356
"cdk/package.json",
357357
"cdk/test/cdk.test.ts",
358358
"cdk/tsconfig.json",
359+
"container/python/Dockerfile",
360+
"container/python/dockerignore.template",
359361
"mcp/python-lambda/README.md",
360362
"mcp/python-lambda/handler.py",
361363
"mcp/python-lambda/pyproject.toml",
@@ -1580,12 +1582,18 @@ def add_numbers(a: int, b: int) -> int:
15801582
return a + b
15811583
15821584
1583-
# Set environment variables for model authentication
1584-
load_model()
1585-
15861585
# Get MCP Toolset
15871586
mcp_toolset = [get_streamable_http_mcp_client()]
15881587
1588+
_credentials_loaded = False
1589+
1590+
def ensure_credentials_loaded():
1591+
global _credentials_loaded
1592+
if not _credentials_loaded:
1593+
load_model()
1594+
_credentials_loaded = True
1595+
1596+
15891597
# Agent Definition
15901598
agent = Agent(
15911599
model=MODEL_ID,
@@ -1598,6 +1606,7 @@ agent = Agent(
15981606
15991607
# Session and Runner
16001608
async def setup_session_and_runner(user_id, session_id):
1609+
ensure_credentials_loaded()
16011610
session_service = InMemorySessionService()
16021611
session = await session_service.create_session(
16031612
app_name=APP_NAME, user_id=user_id, session_id=session_id
@@ -1843,8 +1852,13 @@ from mcp_client.client import get_streamable_http_mcp_client
18431852
app = BedrockAgentCoreApp()
18441853
log = app.logger
18451854
1846-
# Instantiate model
1847-
llm = load_model()
1855+
_llm = None
1856+
1857+
def get_or_create_model():
1858+
global _llm
1859+
if _llm is None:
1860+
_llm = load_model()
1861+
return _llm
18481862
18491863
18501864
# Define a simple function tool
@@ -1869,7 +1883,7 @@ async def invoke(payload, context):
18691883
mcp_tools = await mcp_client.get_tools()
18701884
18711885
# Define the agent using create_react_agent
1872-
graph = create_react_agent(llm, tools=mcp_tools + tools)
1886+
graph = create_react_agent(get_or_create_model(), tools=mcp_tools + tools)
18731887
18741888
# Process the user prompt
18751889
prompt = payload.get("prompt", "What can you help me with?")
@@ -2184,12 +2198,17 @@ from mcp_client.client import get_streamable_http_mcp_client
21842198
app = BedrockAgentCoreApp()
21852199
log = app.logger
21862200
2187-
# Set environment variables for model authentication
2188-
load_model()
2189-
21902201
# Get MCP Server
21912202
mcp_server = get_streamable_http_mcp_client()
21922203
2204+
_credentials_loaded = False
2205+
2206+
def ensure_credentials_loaded():
2207+
global _credentials_loaded
2208+
if not _credentials_loaded:
2209+
load_model()
2210+
_credentials_loaded = True
2211+
21932212
21942213
# Define a simple function tool
21952214
@function_tool
@@ -2200,6 +2219,7 @@ def add_numbers(a: int, b: int) -> int:
22002219
22012220
# Define the agent execution
22022221
async def main(query):
2222+
ensure_credentials_loaded()
22032223
try:
22042224
async with mcp_server as server:
22052225
active_servers = [server] if server else []
@@ -2461,14 +2481,19 @@ def agent_factory():
24612481
return get_or_create_agent
24622482
get_or_create_agent = agent_factory()
24632483
{{else}}
2464-
# Create agent
2465-
agent = Agent(
2466-
model=load_model(),
2467-
system_prompt="""
2468-
You are a helpful assistant. Use tools when appropriate.
2469-
""",
2470-
tools=tools+[mcp_client]
2471-
)
2484+
_agent = None
2485+
2486+
def get_or_create_agent():
2487+
global _agent
2488+
if _agent is None:
2489+
_agent = Agent(
2490+
model=load_model(),
2491+
system_prompt="""
2492+
You are a helpful assistant. Use tools when appropriate.
2493+
""",
2494+
tools=tools+[mcp_client]
2495+
)
2496+
return _agent
24722497
{{/if}}
24732498
24742499
@@ -2480,8 +2505,10 @@ async def invoke(payload, context):
24802505
session_id = getattr(context, 'session_id', 'default-session')
24812506
user_id = getattr(context, 'user_id', 'default-user')
24822507
agent = get_or_create_agent(session_id, user_id)
2483-
2508+
{{else}}
2509+
agent = get_or_create_agent()
24842510
{{/if}}
2511+
24852512
# Execute and format response
24862513
stream = agent.stream_async(payload.get("prompt"))
24872514
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
FROM ghcr.io/astral-sh/uv:python3.12-bookworm-slim
2+
3+
WORKDIR /app
4+
5+
ENV UV_SYSTEM_PYTHON=1 \
6+
UV_COMPILE_BYTECODE=1 \
7+
UV_NO_PROGRESS=1 \
8+
PYTHONUNBUFFERED=1 \
9+
DOCKER_CONTAINER=1
10+
11+
COPY pyproject.toml uv.lock* ./
12+
RUN uv pip install -r pyproject.toml
13+
14+
RUN useradd -m -u 1000 bedrock_agentcore
15+
USER bedrock_agentcore
16+
17+
COPY . .
18+
19+
# 8080: AgentCore runtime endpoint
20+
# 8000: Local dev server (uvicorn)
21+
# 9000: OpenTelemetry collector
22+
EXPOSE 8080 8000 9000
23+
24+
CMD ["opentelemetry-instrument", "python", "-m", "{{entrypoint}}"]
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
# Python
2+
__pycache__/
3+
*.py[cod]
4+
*.egg-info/
5+
.venv/
6+
dist/
7+
build/
8+
9+
# IDE
10+
.vscode/
11+
.idea/
12+
13+
# Testing
14+
.pytest_cache/
15+
.coverage
16+
htmlcov/
17+
18+
# AgentCore build artifacts
19+
.agentcore/artifacts/
20+
*.zip

src/assets/python/googleadk/base/main.py

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -22,12 +22,18 @@ def add_numbers(a: int, b: int) -> int:
2222
return a + b
2323

2424

25-
# Set environment variables for model authentication
26-
load_model()
27-
2825
# Get MCP Toolset
2926
mcp_toolset = [get_streamable_http_mcp_client()]
3027

28+
_credentials_loaded = False
29+
30+
def ensure_credentials_loaded():
31+
global _credentials_loaded
32+
if not _credentials_loaded:
33+
load_model()
34+
_credentials_loaded = True
35+
36+
3137
# Agent Definition
3238
agent = Agent(
3339
model=MODEL_ID,
@@ -40,6 +46,7 @@ def add_numbers(a: int, b: int) -> int:
4046

4147
# Session and Runner
4248
async def setup_session_and_runner(user_id, session_id):
49+
ensure_credentials_loaded()
4350
session_service = InMemorySessionService()
4451
session = await session_service.create_session(
4552
app_name=APP_NAME, user_id=user_id, session_id=session_id

src/assets/python/langchain_langgraph/base/main.py

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,13 @@
99
app = BedrockAgentCoreApp()
1010
log = app.logger
1111

12-
# Instantiate model
13-
llm = load_model()
12+
_llm = None
13+
14+
def get_or_create_model():
15+
global _llm
16+
if _llm is None:
17+
_llm = load_model()
18+
return _llm
1419

1520

1621
# Define a simple function tool
@@ -35,7 +40,7 @@ async def invoke(payload, context):
3540
mcp_tools = await mcp_client.get_tools()
3641

3742
# Define the agent using create_react_agent
38-
graph = create_react_agent(llm, tools=mcp_tools + tools)
43+
graph = create_react_agent(get_or_create_model(), tools=mcp_tools + tools)
3944

4045
# Process the user prompt
4146
prompt = payload.get("prompt", "What can you help me with?")

src/assets/python/openaiagents/base/main.py

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,12 +7,17 @@
77
app = BedrockAgentCoreApp()
88
log = app.logger
99

10-
# Set environment variables for model authentication
11-
load_model()
12-
1310
# Get MCP Server
1411
mcp_server = get_streamable_http_mcp_client()
1512

13+
_credentials_loaded = False
14+
15+
def ensure_credentials_loaded():
16+
global _credentials_loaded
17+
if not _credentials_loaded:
18+
load_model()
19+
_credentials_loaded = True
20+
1621

1722
# Define a simple function tool
1823
@function_tool
@@ -23,6 +28,7 @@ def add_numbers(a: int, b: int) -> int:
2328

2429
# Define the agent execution
2530
async def main(query):
31+
ensure_credentials_loaded()
2632
try:
2733
async with mcp_server as server:
2834
active_servers = [server] if server else []

src/assets/python/strands/base/main.py

Lines changed: 16 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -42,14 +42,19 @@ def get_or_create_agent(session_id, user_id):
4242
return get_or_create_agent
4343
get_or_create_agent = agent_factory()
4444
{{else}}
45-
# Create agent
46-
agent = Agent(
47-
model=load_model(),
48-
system_prompt="""
49-
You are a helpful assistant. Use tools when appropriate.
50-
""",
51-
tools=tools+[mcp_client]
52-
)
45+
_agent = None
46+
47+
def get_or_create_agent():
48+
global _agent
49+
if _agent is None:
50+
_agent = Agent(
51+
model=load_model(),
52+
system_prompt="""
53+
You are a helpful assistant. Use tools when appropriate.
54+
""",
55+
tools=tools+[mcp_client]
56+
)
57+
return _agent
5358
{{/if}}
5459

5560

@@ -61,8 +66,10 @@ async def invoke(payload, context):
6166
session_id = getattr(context, 'session_id', 'default-session')
6267
user_id = getattr(context, 'user_id', 'default-user')
6368
agent = get_or_create_agent(session_id, user_id)
64-
69+
{{else}}
70+
agent = get_or_create_agent()
6571
{{/if}}
72+
6673
# Execute and format response
6774
stream = agent.stream_async(payload.get("prompt"))
6875

src/cli/aws/agentcore.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,11 +10,16 @@ export interface SSELogger {
1010
logSSEEvent(rawLine: string): void;
1111
}
1212

13+
/** Default user ID sent with invocations. Container agents require this to obtain workload access tokens. */
14+
export const DEFAULT_RUNTIME_USER_ID = 'default-user';
15+
1316
export interface InvokeAgentRuntimeOptions {
1417
region: string;
1518
runtimeArn: string;
1619
payload: string;
1720
sessionId?: string;
21+
/** User ID for the runtime invocation. Defaults to 'default-user'. Required for Container agents using identity providers. */
22+
userId?: string;
1823
/** Optional logger for SSE event debugging */
1924
logger?: SSELogger;
2025
}
@@ -112,6 +117,7 @@ export async function invokeAgentRuntimeStreaming(options: InvokeAgentRuntimeOpt
112117
contentType: 'application/json',
113118
accept: 'application/json',
114119
runtimeSessionId: options.sessionId,
120+
runtimeUserId: options.userId ?? DEFAULT_RUNTIME_USER_ID,
115121
});
116122

117123
const response = await client.send(command);
@@ -207,6 +213,7 @@ export async function invokeAgentRuntime(options: InvokeAgentRuntimeOptions): Pr
207213
contentType: 'application/json',
208214
accept: 'application/json',
209215
runtimeSessionId: options.sessionId,
216+
runtimeUserId: options.userId ?? DEFAULT_RUNTIME_USER_ID,
210217
});
211218

212219
const response = await client.send(command);

src/cli/aws/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ export {
1414
type GetAgentRuntimeStatusOptions,
1515
} from './agentcore-control';
1616
export {
17+
DEFAULT_RUNTIME_USER_ID,
1718
invokeAgentRuntime,
1819
invokeAgentRuntimeStreaming,
1920
stopRuntimeSession,

0 commit comments

Comments
 (0)