diff --git a/README.md b/README.md index 53f426695..bd307c634 100644 --- a/README.md +++ b/README.md @@ -115,7 +115,7 @@ agentcore # interactive TUI │ └── delete # delete an evaluator by id ├── project # manage an AgentCore project (scaffold → deploy) │ ├── create # create a project: a managed harness by default, -│ │ # or scaffolded runtime code via --template/--framework; +│ │ # or scaffolded runtime code via --template; │ │ # bare `project create` opens an interactive wizard │ ├── add # add a resource to the project (runtime, harness, memory, …) │ ├── export @@ -185,8 +185,8 @@ one resource of the requested type, `--name` may be omitted. ```bash # Create a project. The default is a harness project: a managed agent -# configured by spec, no model-loop code to maintain. Harness flags -# (--model-id, --max-iterations, --timeout, …) tune it. +# configured by spec, no model-loop code to maintain. Passing only --name +# scaffolds the default harness. agentcore project create --name MyAssistant cd MyAssistant && agentcore project deploy # … or run `agentcore project create` bare in a terminal for the guided @@ -194,22 +194,24 @@ cd MyAssistant && agentcore project deploy # creation path. agentcore harness invoke --id --prompt "hello" -# Scaffold runtime code instead (pass a template or framework flags). +# Scaffold runtime code instead by selecting a template. Templates that support +# a model provider (agent-python-strands) accept --model-provider/--api-key; +# add the -container suffix for a container build, or use `empty` for a project +# with no runtime. agentcore project create --name MyAgent --template agent-python-strands # The same Strands agent built as a container image, with a Dockerfile. agentcore project create --name MyAgent --template agent-python-strands-container -# Translate an existing Amazon Bedrock Agent version into editable runtime code. -# The selected alias identifies the immutable source version; generated code -# invokes models and translated tools directly rather than proxying the alias. -# Use --framework strands (default) or langgraph and optionally select target -# AgentCore Memory. Also available as `project add runtime --type import`. -# The alias must point at a prepared version, not the mutable DRAFT that the -# built-in test alias (TSTALIASID) routes to. Anything that could not be -# translated automatically is listed in the generated IMPORT_NOTES.md. -agentcore project create --name MyImportedAgent --type import \ +# Translate an existing Amazon Bedrock Agent version into editable runtime code +# with `project add runtime --type import` from inside a project. The selected +# alias identifies the immutable source version; generated code invokes models +# and translated tools directly rather than proxying the alias. Use --framework +# strands (default) or langgraph. The alias must point at a prepared version, +# not the mutable DRAFT that the built-in test alias (TSTALIASID) routes to. +# Anything that could not be translated is listed in the generated IMPORT_NOTES.md. +agentcore project add runtime --name MyImportedAgent --type import \ --agent-id A1B2C3D4E5 --agent-alias-id XYZ123ABC4 --region us-east-1 \ - --framework strands --memory none + --framework strands ``` ```bash diff --git a/src/assets/templates/a2a-python-strands/Dockerfile.template b/src/assets/templates/a2a-python-strands/Dockerfile.template deleted file mode 100644 index cb3569eff..000000000 --- a/src/assets/templates/a2a-python-strands/Dockerfile.template +++ /dev/null @@ -1,40 +0,0 @@ -FROM public.ecr.aws/docker/library/python:3.12-slim-trixie - -RUN pip install --no-cache-dir uv - -ARG UV_DEFAULT_INDEX -ARG UV_INDEX - -WORKDIR /app - -ENV UV_SYSTEM_PYTHON=1 \ - UV_COMPILE_BYTECODE=1 \ - UV_NO_PROGRESS=1 \ - PYTHONUNBUFFERED=1 \ - DOCKER_CONTAINER=1 \ - UV_DEFAULT_INDEX=${UV_DEFAULT_INDEX} \ - UV_INDEX=${UV_INDEX} \ - PATH="/app/.venv/bin:$PATH" - -RUN useradd -m -u 1000 bedrock_agentcore - -COPY pyproject.toml uv.lock ./ -RUN uv sync --frozen --no-dev --no-install-project - -COPY --chown=bedrock_agentcore:bedrock_agentcore . . -RUN uv sync --frozen --no-dev - -USER bedrock_agentcore - -# AgentCore Runtime service contract ports -# https://docs.aws.amazon.com/bedrock-agentcore/latest/devguide/runtime-service-contract.html -# 8080: HTTP Mode -# 8000: MCP Mode -# 9000: A2A Mode -EXPOSE 8080 8000 9000 - -{{#if enableOtel}} -CMD ["opentelemetry-instrument", "python", "-m", "{{entrypoint}}"] -{{else}} -CMD ["python", "-m", "{{entrypoint}}"] -{{/if}} diff --git a/src/assets/templates/a2a-python-strands/dockerignore.template b/src/assets/templates/a2a-python-strands/dockerignore.template deleted file mode 100644 index a0c4eb658..000000000 --- a/src/assets/templates/a2a-python-strands/dockerignore.template +++ /dev/null @@ -1,27 +0,0 @@ -# Python -__pycache__/ -*.py[cod] -*.egg-info/ -.venv/ -dist/ -build/ - -# IDE -.vscode/ -.idea/ - -# Testing -.pytest_cache/ -.coverage -htmlcov/ - -# Secrets and environment files -.env -.env.* - -# Version control -.git/ - -# AgentCore build artifacts -.agentcore/artifacts/ -*.zip diff --git a/src/assets/templates/a2a-python-strands/main.py b/src/assets/templates/a2a-python-strands/main.py index 7e6a032d3..649f96411 100644 --- a/src/assets/templates/a2a-python-strands/main.py +++ b/src/assets/templates/a2a-python-strands/main.py @@ -2,9 +2,7 @@ from strands.multiagent.a2a.executor import StrandsA2AExecutor from bedrock_agentcore.runtime import serve_a2a from model.load import load_model -{{#if hasMemory}} from memory.session import get_memory_session_manager -{{/if}} {{#if needsOs}} import os {{/if}} @@ -90,9 +88,7 @@ def list_files(path: str) -> str: agent = Agent( name="{{ name }}", model=load_model(), -{{#if hasMemory}} session_manager=get_memory_session_manager("default-session", "default-user"), -{{/if}} system_prompt=SYSTEM_PROMPT, tools=tools, ) diff --git a/src/assets/templates/a2a-python-strands/memory/session.py b/src/assets/templates/a2a-python-strands/memory/session.py index 20e105674..6372a2f68 100644 --- a/src/assets/templates/a2a-python-strands/memory/session.py +++ b/src/assets/templates/a2a-python-strands/memory/session.py @@ -2,7 +2,7 @@ import uuid from typing import Optional -from bedrock_agentcore.memory.integrations.strands.config import AgentCoreMemoryConfig{{#if memoryStrategies.length}}, RetrievalConfig{{/if}} +from bedrock_agentcore.memory.integrations.strands.config import AgentCoreMemoryConfig, RetrievalConfig from bedrock_agentcore.memory.integrations.strands.session_manager import AgentCoreMemorySessionManager MEMORY_ID = os.getenv("{{memoryEnvVarName}}") @@ -17,31 +17,19 @@ def get_memory_session_manager( session_id = session_id or uuid.uuid4().hex -{{#if memoryStrategies.length}} retrieval_config = { -{{#if (includes memoryStrategies "SEMANTIC")}} f"/users/{actor_id}/facts": RetrievalConfig(top_k=3, relevance_score=0.5), -{{/if}} -{{#if (includes memoryStrategies "USER_PREFERENCE")}} f"/users/{actor_id}/preferences": RetrievalConfig(top_k=3, relevance_score=0.5), -{{/if}} -{{#if (includes memoryStrategies "EPISODIC")}} f"/episodes/{actor_id}/{session_id}": RetrievalConfig(top_k=5, relevance_score=0.5), -{{/if}} -{{#if (includes memoryStrategies "SUMMARIZATION")}} f"/summaries/{actor_id}": RetrievalConfig(top_k=3, relevance_score=0.5), -{{/if}} } -{{/if}} return AgentCoreMemorySessionManager( AgentCoreMemoryConfig( memory_id=MEMORY_ID, session_id=session_id, actor_id=actor_id, -{{#if memoryStrategies.length}} retrieval_config=retrieval_config, -{{/if}} ), REGION, ) diff --git a/src/assets/templates/a2a-python-strands/model/load.py b/src/assets/templates/a2a-python-strands/model/load.py index 05da58b20..07b60a420 100644 --- a/src/assets/templates/a2a-python-strands/model/load.py +++ b/src/assets/templates/a2a-python-strands/model/load.py @@ -1,239 +1,6 @@ -{{#if (eq modelProvider "Bedrock")}} -{{#if bedrockMantle}} -import os - -from aws_bedrock_token_generator import provide_token -{{#if (eq mantleApiFormat "chat_completions")}} -from strands.models.openai import OpenAIModel -{{else}} -{{#if mantleProprietary}} -from strands.models.openai_responses import OpenAIResponsesModel -{{else}} -from model.mantle_compat import MantleCompatResponsesModel -{{/if}} -{{/if}} - -MODEL_ID = "{{modelId}}" - - -def load_model(): - """ - Get a Bedrock Mantle model client. These OpenAI-compatible models (e.g. openai.gpt-5.5, - openai.gpt-oss-120b) are served via the Bedrock Mantle endpoint, NOT the Converse API — so they - are invoked through an OpenAI-style client authenticated with a short-lived Bedrock bearer token. - Region is read from AWS_REGION (set by the AgentCore runtime). - """ - region = os.environ.get("AWS_REGION", os.environ.get("AWS_DEFAULT_REGION", "us-east-1")) - token = provide_token(region=region) - {{#if mantleProprietary}} - # Proprietary OpenAI models only work on the /openai/v1 Mantle path. - base_url = f"https://bedrock-mantle.{region}.api.aws/openai/v1" - {{else}} - # Open-source OpenAI models (gpt-oss-*) only work on the /v1 Mantle path. - base_url = f"https://bedrock-mantle.{region}.api.aws/v1" - {{/if}} - client_args = {"api_key": token, "base_url": base_url} - - params = {} - {{#if modelMaxTokens}} - {{#if (eq mantleApiFormat "chat_completions")}} - params["max_completion_tokens"] = {{modelMaxTokens}} - {{else}} - params["max_output_tokens"] = {{modelMaxTokens}} - {{/if}} - {{/if}} - {{#if modelTemperature}} - params["temperature"] = {{modelTemperature}} - {{/if}} - {{#if modelTopP}} - params["top_p"] = {{modelTopP}} - {{/if}} - {{#if (eq mantleApiFormat "chat_completions")}} - return OpenAIModel(client_args=client_args, model_id=MODEL_ID, params=params) - {{else}} - # Responses API: Mantle does not persist responses, so disable server-side storage. - params["store"] = False - {{#if mantleProprietary}} - return OpenAIResponsesModel(client_args=client_args, model_id=MODEL_ID, params=params) - {{else}} - return MantleCompatResponsesModel(client_args=client_args, model_id=MODEL_ID, params=params) - {{/if}} - {{/if}} -{{else}} from strands.models.bedrock import BedrockModel def load_model() -> BedrockModel: """Get Bedrock model client using IAM credentials.""" - return BedrockModel(model_id="{{#if modelId}}{{modelId}}{{else}}global.anthropic.claude-sonnet-4-5-20250929-v1:0{{/if}}"{{#if modelMaxTokens}}, max_tokens={{modelMaxTokens}}{{/if}}{{#if modelTemperature}}, temperature={{modelTemperature}}{{/if}}{{#if modelTopP}}, top_p={{modelTopP}}{{/if}}) -{{/if}} -{{/if}} -{{#if (eq modelProvider "Anthropic")}} -import os - -from strands.models.anthropic import AnthropicModel -from bedrock_agentcore.identity.auth import requires_api_key - -IDENTITY_PROVIDER_NAME = "{{identityProviders.[0].name}}" -IDENTITY_ENV_VAR = "{{identityProviders.[0].envVarName}}" - - -@requires_api_key(provider_name=IDENTITY_PROVIDER_NAME) -def _agentcore_identity_api_key_provider(api_key: str) -> str: - """Fetch API key from AgentCore Identity.""" - return api_key - - -def _get_api_key() -> str: - """ - Uses AgentCore Identity for API key management in deployed environments. - For local development, run via 'agentcore dev' which loads agentcore/.env. - """ - if os.getenv("LOCAL_DEV") == "1": - api_key = os.getenv(IDENTITY_ENV_VAR) - if not api_key: - raise RuntimeError( - f"{IDENTITY_ENV_VAR} not found. Add {IDENTITY_ENV_VAR}=your-key to .env.local" - ) - return api_key - return _agentcore_identity_api_key_provider() - - -def load_model() -> AnthropicModel: - """Get authenticated Anthropic model client.""" - return AnthropicModel( - client_args={"api_key": _get_api_key()}, - model_id="claude-sonnet-4-5-20250929", - max_tokens=5000, - ) -{{/if}} -{{#if (eq modelProvider "OpenAI")}} -import os - -from strands.models.openai import OpenAIModel -from bedrock_agentcore.identity.auth import requires_api_key - -IDENTITY_PROVIDER_NAME = "{{identityProviders.[0].name}}" -IDENTITY_ENV_VAR = "{{identityProviders.[0].envVarName}}" - - -@requires_api_key(provider_name=IDENTITY_PROVIDER_NAME) -def _agentcore_identity_api_key_provider(api_key: str) -> str: - """Fetch API key from AgentCore Identity.""" - return api_key - - -def _get_api_key() -> str: - """ - Uses AgentCore Identity for API key management in deployed environments. - For local development, run via 'agentcore dev' which loads agentcore/.env. - """ - if os.getenv("LOCAL_DEV") == "1": - api_key = os.getenv(IDENTITY_ENV_VAR) - if not api_key: - raise RuntimeError( - f"{IDENTITY_ENV_VAR} not found. Add {IDENTITY_ENV_VAR}=your-key to .env.local" - ) - return api_key - return _agentcore_identity_api_key_provider() - - -def load_model() -> OpenAIModel: - """Get authenticated OpenAI model client.""" - return OpenAIModel( - client_args={"api_key": _get_api_key()}, - model_id="{{#if modelId}}{{modelId}}{{else}}gpt-4.1{{/if}}", - ) -{{/if}} -{{#if (eq modelProvider "Gemini")}} -import os - -from strands.models.gemini import GeminiModel -from bedrock_agentcore.identity.auth import requires_api_key - -IDENTITY_PROVIDER_NAME = "{{identityProviders.[0].name}}" -IDENTITY_ENV_VAR = "{{identityProviders.[0].envVarName}}" - - -@requires_api_key(provider_name=IDENTITY_PROVIDER_NAME) -def _agentcore_identity_api_key_provider(api_key: str) -> str: - """Fetch API key from AgentCore Identity.""" - return api_key - - -def _get_api_key() -> str: - """ - Uses AgentCore Identity for API key management in deployed environments. - For local development, run via 'agentcore dev' which loads agentcore/.env. - """ - if os.getenv("LOCAL_DEV") == "1": - api_key = os.getenv(IDENTITY_ENV_VAR) - if not api_key: - raise RuntimeError( - f"{IDENTITY_ENV_VAR} not found. Add {IDENTITY_ENV_VAR}=your-key to .env.local" - ) - return api_key - return _agentcore_identity_api_key_provider() - - -def load_model() -> GeminiModel: - """Get authenticated Gemini model client.""" - return GeminiModel( - client_args={"api_key": _get_api_key()}, - model_id="{{#if modelId}}{{modelId}}{{else}}gemini-2.5-flash{{/if}}", - ) -{{/if}} -{{#if (eq modelProvider "LiteLLM")}} -import os -{{#if litellmAdditionalParams}} -import json -{{/if}} - -from strands.models.litellm import LiteLLMModel -{{#if identityProviders.[0].name}} -from bedrock_agentcore.identity.auth import requires_api_key - -IDENTITY_PROVIDER_NAME = "{{identityProviders.[0].name}}" -IDENTITY_ENV_VAR = "{{identityProviders.[0].envVarName}}" - - -@requires_api_key(provider_name=IDENTITY_PROVIDER_NAME) -def _agentcore_identity_api_key_provider(api_key: str) -> str: - """Fetch API key from AgentCore Identity.""" - return api_key - - -def _get_api_key() -> str: - """ - Uses AgentCore Identity for API key management in deployed environments. - For local development, run via 'agentcore dev' which loads agentcore/.env. - """ - if os.getenv("LOCAL_DEV") == "1": - api_key = os.getenv(IDENTITY_ENV_VAR) - if not api_key: - raise RuntimeError( - f"{IDENTITY_ENV_VAR} not found. Add {IDENTITY_ENV_VAR}=your-key to .env.local" - ) - return api_key - return _agentcore_identity_api_key_provider() -{{/if}} - - - - -def load_model() -> LiteLLMModel: - """Get a LiteLLM model client (proxies to the provider encoded in model_id).""" - client_args = {} - {{#if identityProviders.[0].name}} - client_args["api_key"] = _get_api_key() - {{/if}} - {{#if litellmApiBase}} - client_args["api_base"] = {{safeJson litellmApiBase}} - {{/if}} - params = {{#if litellmAdditionalParams}}json.loads({{pyJsonStr litellmAdditionalParams}}){{else}}{}{{/if}} - return LiteLLMModel( - client_args=client_args, - model_id="{{#if modelId}}{{modelId}}{{else}}bedrock/us.anthropic.claude-sonnet-4-5-20250514-v1:0{{/if}}", - params=params, - ) -{{/if}} + return BedrockModel(model_id="global.anthropic.claude-sonnet-4-5-20250929-v1:0") diff --git a/src/assets/templates/a2a-python-strands/pyproject.toml b/src/assets/templates/a2a-python-strands/pyproject.toml index 85aa6b900..6c0d8f66e 100644 --- a/src/assets/templates/a2a-python-strands/pyproject.toml +++ b/src/assets/templates/a2a-python-strands/pyproject.toml @@ -13,12 +13,7 @@ dependencies = [ "aws-opentelemetry-distro ~= 0.17.0", "bedrock-agentcore[a2a] ~= 1.9.1", "botocore[crt] ~= 1.43.0", - {{#if (eq modelProvider "Anthropic")}}"strands-agents[anthropic] ~= 1.15.0", - {{else}}{{#if (eq modelProvider "OpenAI")}}"strands-agents[openai] ~= 1.15.0", - {{else}}{{#if (eq modelProvider "Gemini")}}"strands-agents[gemini] ~= 1.15.0", - {{else}}{{#if (eq modelProvider "LiteLLM")}}"strands-agents[litellm] ~= 1.15.0", - {{else}}"strands-agents ~= 1.15.0", - {{/if}}{{/if}}{{/if}}{{/if}} + "strands-agents ~= 1.15.0", ] [tool.hatch.build.targets.wheel] diff --git a/src/assets/templates/agent-python/README.md b/src/assets/templates/agent-python-minimal/README.md similarity index 97% rename from src/assets/templates/agent-python/README.md rename to src/assets/templates/agent-python-minimal/README.md index ed945bd99..2093a041f 100644 --- a/src/assets/templates/agent-python/README.md +++ b/src/assets/templates/agent-python-minimal/README.md @@ -1,4 +1,4 @@ -# agent-python +# agent-python-minimal A minimal AgentCore Runtime HTTP agent with no agent framework. Its `@app.entrypoint` returns a fixed `Hello, world!` message for every diff --git a/src/assets/templates/agent-python/main.py b/src/assets/templates/agent-python-minimal/main.py similarity index 100% rename from src/assets/templates/agent-python/main.py rename to src/assets/templates/agent-python-minimal/main.py diff --git a/src/assets/templates/agent-python/pyproject.toml b/src/assets/templates/agent-python-minimal/pyproject.toml similarity index 92% rename from src/assets/templates/agent-python/pyproject.toml rename to src/assets/templates/agent-python-minimal/pyproject.toml index 684d8f604..c7b4e3ca2 100644 --- a/src/assets/templates/agent-python/pyproject.toml +++ b/src/assets/templates/agent-python-minimal/pyproject.toml @@ -3,7 +3,7 @@ requires = ["hatchling"] build-backend = "hatchling.build" [project] -name = "agent-python" +name = "agent-python-minimal" version = "0.1.0" description = "Minimal AgentCore Runtime HTTP agent" readme = "README.md" diff --git a/src/assets/templates/agent-python-strands/main.py b/src/assets/templates/agent-python-strands/main.py index 7988bae03..9a350a3f3 100644 --- a/src/assets/templates/agent-python-strands/main.py +++ b/src/assets/templates/agent-python-strands/main.py @@ -1,13 +1,10 @@ from typing import Any -from collections import OrderedDict from strands import Agent, tool from strands.agent.conversation_manager.null_conversation_manager import NullConversationManager from bedrock_agentcore.runtime import BedrockAgentCoreApp from model.load import load_model -{{#if hasMemory}} from memory.session import get_memory_session_manager -{{/if}} app = BedrockAgentCoreApp() log = app.logger @@ -32,7 +29,6 @@ def _make_conversation_manager(): return NullConversationManager() -{{#if hasMemory}} def agent_factory(): cache = {} def get_or_create_agent(session_id, user_id): @@ -48,30 +44,6 @@ def get_or_create_agent(session_id, user_id): return cache[key] return get_or_create_agent get_or_create_agent = agent_factory() -{{else}} -# Reuses one Agent per session_id so each session keeps its own in-process -# conversation history (best-effort; resets on cold start). The cache is bounded -# to 128 sessions with LRU eviction (least-recently-used is dropped and its -# history reset) so a single process serving many sessions cannot leak history -# between them or grow without limit. For durable history, attach a session manager. -def agent_factory(): - cache = OrderedDict() - def get_or_create_agent(session_id): - if session_id in cache: - cache.move_to_end(session_id) - return cache[session_id] - if len(cache) >= 128: - cache.popitem(last=False) - cache[session_id] = Agent( - model=load_model(), - system_prompt=DEFAULT_SYSTEM_PROMPT, - tools=tools, - conversation_manager=_make_conversation_manager(), - ) - return cache[session_id] - return get_or_create_agent -get_or_create_agent = agent_factory() -{{/if}} def strip_trailing_tool_use(messages: Any) -> list[dict]: @@ -116,12 +88,8 @@ async def invoke(payload, context): log.info("Invoking Agent.....") session_id = getattr(context, "session_id", "default-session") - {{#if hasMemory}} user_id = getattr(context, "user_id", "default-user") agent = get_or_create_agent(session_id, user_id) - {{else}} - agent = get_or_create_agent(session_id) - {{/if}} prompt = _extract_prompt(payload) diff --git a/src/assets/templates/agent-python-strands/memory/session.py b/src/assets/templates/agent-python-strands/memory/session.py index 20e105674..6372a2f68 100644 --- a/src/assets/templates/agent-python-strands/memory/session.py +++ b/src/assets/templates/agent-python-strands/memory/session.py @@ -2,7 +2,7 @@ import uuid from typing import Optional -from bedrock_agentcore.memory.integrations.strands.config import AgentCoreMemoryConfig{{#if memoryStrategies.length}}, RetrievalConfig{{/if}} +from bedrock_agentcore.memory.integrations.strands.config import AgentCoreMemoryConfig, RetrievalConfig from bedrock_agentcore.memory.integrations.strands.session_manager import AgentCoreMemorySessionManager MEMORY_ID = os.getenv("{{memoryEnvVarName}}") @@ -17,31 +17,19 @@ def get_memory_session_manager( session_id = session_id or uuid.uuid4().hex -{{#if memoryStrategies.length}} retrieval_config = { -{{#if (includes memoryStrategies "SEMANTIC")}} f"/users/{actor_id}/facts": RetrievalConfig(top_k=3, relevance_score=0.5), -{{/if}} -{{#if (includes memoryStrategies "USER_PREFERENCE")}} f"/users/{actor_id}/preferences": RetrievalConfig(top_k=3, relevance_score=0.5), -{{/if}} -{{#if (includes memoryStrategies "EPISODIC")}} f"/episodes/{actor_id}/{session_id}": RetrievalConfig(top_k=5, relevance_score=0.5), -{{/if}} -{{#if (includes memoryStrategies "SUMMARIZATION")}} f"/summaries/{actor_id}": RetrievalConfig(top_k=3, relevance_score=0.5), -{{/if}} } -{{/if}} return AgentCoreMemorySessionManager( AgentCoreMemoryConfig( memory_id=MEMORY_ID, session_id=session_id, actor_id=actor_id, -{{#if memoryStrategies.length}} retrieval_config=retrieval_config, -{{/if}} ), REGION, ) diff --git a/src/assets/templates/agent-python/Dockerfile.template b/src/assets/templates/agent-python/Dockerfile.template deleted file mode 100644 index 08a64f467..000000000 --- a/src/assets/templates/agent-python/Dockerfile.template +++ /dev/null @@ -1,39 +0,0 @@ -FROM public.ecr.aws/docker/library/python:3.12-slim-trixie - -RUN pip install --no-cache-dir uv - -ARG UV_DEFAULT_INDEX -ARG UV_INDEX - -WORKDIR /app - -ENV UV_SYSTEM_PYTHON=1 \ - UV_COMPILE_BYTECODE=1 \ - UV_NO_PROGRESS=1 \ - PYTHONUNBUFFERED=1 \ - DOCKER_CONTAINER=1 \ - UV_DEFAULT_INDEX=${UV_DEFAULT_INDEX} \ - UV_INDEX=${UV_INDEX} \ - PATH="/app/.venv/bin:$PATH" - -RUN useradd -m -u 1000 bedrock_agentcore - -# Install dependencies first so code changes don't invalidate the layer. -COPY pyproject.toml uv.lock ./ -RUN uv sync --frozen --no-dev --no-install-project - -COPY --chown=bedrock_agentcore:bedrock_agentcore . . -RUN uv sync --frozen --no-dev - -USER bedrock_agentcore - -# AgentCore Runtime service contract ports -# https://docs.aws.amazon.com/bedrock-agentcore/latest/devguide/runtime-service-contract.html -# 8080: HTTP Mode -# 8000: MCP Mode -# 9000: A2A Mode -EXPOSE 8080 8000 9000 - -# opentelemetry-instrument (from aws-opentelemetry-distro) starts a real -# TracerProvider; plain `python -m main` would export nothing. -CMD ["opentelemetry-instrument", "python", "-m", "main"] diff --git a/src/assets/templates/agent-python/dockerignore.template b/src/assets/templates/agent-python/dockerignore.template deleted file mode 100644 index a0c4eb658..000000000 --- a/src/assets/templates/agent-python/dockerignore.template +++ /dev/null @@ -1,27 +0,0 @@ -# Python -__pycache__/ -*.py[cod] -*.egg-info/ -.venv/ -dist/ -build/ - -# IDE -.vscode/ -.idea/ - -# Testing -.pytest_cache/ -.coverage -htmlcov/ - -# Secrets and environment files -.env -.env.* - -# Version control -.git/ - -# AgentCore build artifacts -.agentcore/artifacts/ -*.zip diff --git a/src/assets/templates/agent-typescript-strands/Dockerfile.template b/src/assets/templates/agent-typescript-strands/Dockerfile.template deleted file mode 100644 index df9c6bac1..000000000 --- a/src/assets/templates/agent-typescript-strands/Dockerfile.template +++ /dev/null @@ -1,25 +0,0 @@ -FROM public.ecr.aws/docker/library/node:22-slim - -WORKDIR /app - -ENV NODE_ENV=production \ - DOCKER_CONTAINER=1 - -RUN userdel -r node 2>/dev/null || true -RUN useradd -m -u 1000 bedrock_agentcore - -COPY package.json package-lock.json* ./ -RUN npm ci --omit=dev || npm install --omit=dev - -COPY --chown=bedrock_agentcore:bedrock_agentcore . . - -USER bedrock_agentcore - -# AgentCore Runtime service contract ports -# https://docs.aws.amazon.com/bedrock-agentcore/latest/devguide/runtime-service-contract.html -# 8080: HTTP Mode -# 8000: MCP Mode -# 9000: A2A Mode -EXPOSE 8080 8000 9000 - -CMD ["npx", "tsx", "main.ts"] diff --git a/src/assets/templates/agent-typescript-strands/README.md b/src/assets/templates/agent-typescript-strands/README.md index 4c64ecd57..e5fe71019 100644 --- a/src/assets/templates/agent-typescript-strands/README.md +++ b/src/assets/templates/agent-typescript-strands/README.md @@ -18,13 +18,6 @@ defines an HTTP server that streams tokens from your chosen Agent framework SDK. The generated Zod request schema keeps plain prompts typed as strings before forwarding them to Strands. Retain this validation when extending the request shape, and pass only prompt text to the agent. -## Environment Variables - -| Variable | Required | Description | -| --- | --- | --- | -{{#if identityProviders.[0]}}| `{{identityProviders.[0].envVarName}}` | Yes | {{modelProvider}} API key (local) or Identity provider name (deployed) | -{{/if}}| `LOCAL_DEV` | No | Set to `1` to use `.env.local` instead of AgentCore Identity | - # Developing locally If installation was successful, `node_modules/` is already populated with dependencies. diff --git a/src/assets/templates/agent-typescript-strands/dockerignore.template b/src/assets/templates/agent-typescript-strands/dockerignore.template deleted file mode 100644 index 4fe494a08..000000000 --- a/src/assets/templates/agent-typescript-strands/dockerignore.template +++ /dev/null @@ -1,24 +0,0 @@ -# Node -node_modules/ -dist/ -npm-debug.log* -yarn-debug.log* -yarn-error.log* - -# IDE -.vscode/ -.idea/ - -# Testing -coverage/ - -# Secrets and environment files -.env -.env.* - -# Version control -.git/ - -# AgentCore build artifacts -.agentcore/artifacts/ -*.zip diff --git a/src/assets/templates/agent-typescript-strands/main.ts b/src/assets/templates/agent-typescript-strands/main.ts index d30a60af0..de0cb3deb 100644 --- a/src/assets/templates/agent-typescript-strands/main.ts +++ b/src/assets/templates/agent-typescript-strands/main.ts @@ -3,9 +3,7 @@ import { Agent, McpClient, tool, type ToolList } from '@strands-agents/sdk'; import { z } from 'zod'; import { loadModel } from './model/load.js'; import { getStreamableHttpMcpClient } from './mcp_client/client.js'; -{{#if hasMemory}} import { getActorId, getOrCreateMemoryManager } from './memory/memory.js'; -{{/if}} // Define a collection of MCP clients (filter out anything that failed to initialize) const mcpClients: McpClient[] = [getStreamableHttpMcpClient()].filter( @@ -39,7 +37,6 @@ const requestSchema = z.object({ userId: z.string().optional(), }); -{{#if hasMemory}} const agentCache = new Map(); async function getOrCreateAgent(sessionId: string, actorId: string): Promise { @@ -57,53 +54,15 @@ async function getOrCreateAgent(sessionId: string, actorId: string): Promise(); - -async function getOrCreateAgent(sessionId: string): Promise { - const existing = agentCache.get(sessionId); - if (existing) { - agentCache.delete(sessionId); - agentCache.set(sessionId, existing); - return existing; - } - if (agentCache.size >= AGENT_CACHE_LIMIT) { - const oldest = agentCache.keys().next().value; - if (oldest !== undefined) agentCache.delete(oldest); - } - const model = await loadModel(); - const agent = new Agent({ - model, - systemPrompt: SYSTEM_PROMPT, - tools, - }); - agentCache.set(sessionId, agent); - return agent; -} -{{/if}} const app = new BedrockAgentCoreApp({ invocationHandler: { requestSchema, async *process(payload, context) { - {{#if hasMemory}} const sessionId = context?.sessionId ?? 'default-session'; const actorId = getActorId(payload, context); const agent = await getOrCreateAgent(sessionId, actorId); - {{else}} - const sessionId = context?.sessionId ?? 'default-session'; - const agent = await getOrCreateAgent(sessionId); - {{/if}} - {{#if hasMemory}} try { for await (const event of agent.stream(payload.prompt)) { if ( @@ -120,29 +79,6 @@ const app = new BedrockAgentCoreApp({ // it, an idle reclamation can lose the tail of the conversation. await agent.memoryManager?.flush(); } - {{else}} - // Snapshot history before streaming so a failed turn can be rolled back. - // Agent.stream() appends the user message before invoking the model; on a - // mid-stream error that user turn would otherwise linger in the cached - // agent, and the next turn for this session would send consecutive user - // messages (rejected by providers that require strict role alternation, - // e.g. Anthropic). Restoring on error keeps the session reusable. - const snapshot = agent.takeSnapshot({ include: ['messages'] }); - try { - for await (const event of agent.stream(payload.prompt)) { - if ( - event.type === 'modelStreamUpdateEvent' && - event.event?.type === 'modelContentBlockDeltaEvent' && - event.event.delta?.type === 'textDelta' - ) { - yield { data: event.event.delta.text }; - } - } - } catch (error) { - agent.loadSnapshot(snapshot); - throw error; - } - {{/if}} }, }, }); diff --git a/src/assets/templates/agent-typescript-strands/memory/memory.ts b/src/assets/templates/agent-typescript-strands/memory/memory.ts index 0874fb4db..54a55fc99 100644 --- a/src/assets/templates/agent-typescript-strands/memory/memory.ts +++ b/src/assets/templates/agent-typescript-strands/memory/memory.ts @@ -28,18 +28,10 @@ export function getOrCreateMemoryManager(sessionId: string, actorId: string): Me actorId, sessionId, namespaces: [ -{{#if (includes memoryStrategies "SEMANTIC")}} { namespace: '/users/{actorId}/facts' }, -{{/if}} -{{#if (includes memoryStrategies "USER_PREFERENCE")}} { namespace: '/users/{actorId}/preferences' }, -{{/if}} -{{#if (includes memoryStrategies "EPISODIC")}} { namespace: '/episodes/{actorId}/{sessionId}' }, -{{/if}} -{{#if (includes memoryStrategies "SUMMARIZATION")}} { namespace: '/summaries/{actorId}/{sessionId}' }, -{{/if}} ], // readMode defaults to 'per-namespace' (one retrieve call per namespace). // Switch to 'subtree' to consolidate to a single hierarchical recall call. diff --git a/src/assets/templates/agent-typescript-strands/model/load.ts b/src/assets/templates/agent-typescript-strands/model/load.ts index 00e22cd9b..d95f28c7f 100644 --- a/src/assets/templates/agent-typescript-strands/model/load.ts +++ b/src/assets/templates/agent-typescript-strands/model/load.ts @@ -1,102 +1,5 @@ -{{#if (eq modelProvider "Bedrock")}} import { BedrockModel } from '@strands-agents/sdk/models/bedrock'; export function loadModel(): BedrockModel { return new BedrockModel({ modelId: 'global.anthropic.claude-sonnet-4-5-20250929-v1:0' }); } -{{/if}} -{{#if (eq modelProvider "Anthropic")}} -import { AnthropicModel } from '@strands-agents/sdk/models/anthropic'; -import { withApiKey } from 'bedrock-agentcore/identity'; - -const IDENTITY_PROVIDER_NAME = '{{identityProviders.[0].name}}'; -const IDENTITY_ENV_VAR = '{{identityProviders.[0].envVarName}}'; - -async function getApiKey(): Promise { - if (process.env.LOCAL_DEV === '1') { - const apiKey = process.env[IDENTITY_ENV_VAR] ?? process.env.ANTHROPIC_API_KEY; - if (!apiKey) { - throw new Error(`${IDENTITY_ENV_VAR} or ANTHROPIC_API_KEY not found. Add your key to agentcore/.env.local`); - } - return apiKey; - } - return withApiKey({ providerName: IDENTITY_PROVIDER_NAME })(async (apiKey: string) => apiKey)(); -} - -let _model: AnthropicModel | undefined; - -export async function loadModel(): Promise { - if (!_model) { - const apiKey = await getApiKey(); - _model = new AnthropicModel({ - apiKey, - modelId: 'claude-sonnet-4-5-20250929', - maxTokens: 5000, - }); - } - return _model; -} -{{/if}} -{{#if (eq modelProvider "OpenAI")}} -import { OpenAIModel } from '@strands-agents/sdk/models/openai'; -import { withApiKey } from 'bedrock-agentcore/identity'; - -const IDENTITY_PROVIDER_NAME = '{{identityProviders.[0].name}}'; -const IDENTITY_ENV_VAR = '{{identityProviders.[0].envVarName}}'; - -async function getApiKey(): Promise { - if (process.env.LOCAL_DEV === '1') { - const apiKey = process.env[IDENTITY_ENV_VAR] ?? process.env.OPENAI_API_KEY; - if (!apiKey) { - throw new Error(`${IDENTITY_ENV_VAR} or OPENAI_API_KEY not found. Add your key to agentcore/.env.local`); - } - return apiKey; - } - return withApiKey({ providerName: IDENTITY_PROVIDER_NAME })(async (apiKey: string) => apiKey)(); -} - -let _model: OpenAIModel | undefined; - -export async function loadModel(): Promise { - if (!_model) { - const apiKey = await getApiKey(); - _model = new OpenAIModel({ - api: 'chat', - apiKey, - modelId: 'gpt-4.1', - }); - } - return _model; -} -{{/if}} -{{#if (eq modelProvider "Gemini")}} -import { GoogleModel } from '@strands-agents/sdk/models/google'; -import { withApiKey } from 'bedrock-agentcore/identity'; - -const IDENTITY_PROVIDER_NAME = '{{identityProviders.[0].name}}'; -const IDENTITY_ENV_VAR = '{{identityProviders.[0].envVarName}}'; - -async function getApiKey(): Promise { - if (process.env.LOCAL_DEV === '1') { - const apiKey = process.env[IDENTITY_ENV_VAR] ?? process.env.GEMINI_API_KEY; - if (!apiKey) { - throw new Error(`${IDENTITY_ENV_VAR} or GEMINI_API_KEY not found. Add your key to agentcore/.env.local`); - } - return apiKey; - } - return withApiKey({ providerName: IDENTITY_PROVIDER_NAME })(async (apiKey: string) => apiKey)(); -} - -let _model: GoogleModel | undefined; - -export async function loadModel(): Promise { - if (!_model) { - const apiKey = await getApiKey(); - _model = new GoogleModel({ - apiKey, - modelId: 'gemini-2.5-flash', - }); - } - return _model; -} -{{/if}} diff --git a/src/assets/templates/agent-typescript-strands/package.json.template b/src/assets/templates/agent-typescript-strands/package.json.template index 804fec0f1..77eb3bc8e 100644 --- a/src/assets/templates/agent-typescript-strands/package.json.template +++ b/src/assets/templates/agent-typescript-strands/package.json.template @@ -10,14 +10,11 @@ "dev": "tsx watch main.ts" }, "dependencies": { - {{#if (eq modelProvider "Anthropic")}}"@anthropic-ai/sdk": "~0.92.0", - {{/if}}{{#if (eq modelProvider "Gemini")}}"@google/genai": "~1.40.0", - {{/if}}"@modelcontextprotocol/sdk": "~1.25.2", + "@modelcontextprotocol/sdk": "~1.25.2", "@opentelemetry/api": "~1.9.0", "@strands-agents/sdk": "~1.5.0", "bedrock-agentcore": "~0.3.0", - {{#if (eq modelProvider "OpenAI")}}"openai": "~6.7.0", - {{/if}}"tsx": "~4.19.0", + "tsx": "~4.19.0", "zod": "~4.4.3" }, "devDependencies": { diff --git a/src/assets/templates/agui-python-strands/Dockerfile.template b/src/assets/templates/agui-python-strands/Dockerfile.template deleted file mode 100644 index cb3569eff..000000000 --- a/src/assets/templates/agui-python-strands/Dockerfile.template +++ /dev/null @@ -1,40 +0,0 @@ -FROM public.ecr.aws/docker/library/python:3.12-slim-trixie - -RUN pip install --no-cache-dir uv - -ARG UV_DEFAULT_INDEX -ARG UV_INDEX - -WORKDIR /app - -ENV UV_SYSTEM_PYTHON=1 \ - UV_COMPILE_BYTECODE=1 \ - UV_NO_PROGRESS=1 \ - PYTHONUNBUFFERED=1 \ - DOCKER_CONTAINER=1 \ - UV_DEFAULT_INDEX=${UV_DEFAULT_INDEX} \ - UV_INDEX=${UV_INDEX} \ - PATH="/app/.venv/bin:$PATH" - -RUN useradd -m -u 1000 bedrock_agentcore - -COPY pyproject.toml uv.lock ./ -RUN uv sync --frozen --no-dev --no-install-project - -COPY --chown=bedrock_agentcore:bedrock_agentcore . . -RUN uv sync --frozen --no-dev - -USER bedrock_agentcore - -# AgentCore Runtime service contract ports -# https://docs.aws.amazon.com/bedrock-agentcore/latest/devguide/runtime-service-contract.html -# 8080: HTTP Mode -# 8000: MCP Mode -# 9000: A2A Mode -EXPOSE 8080 8000 9000 - -{{#if enableOtel}} -CMD ["opentelemetry-instrument", "python", "-m", "{{entrypoint}}"] -{{else}} -CMD ["python", "-m", "{{entrypoint}}"] -{{/if}} diff --git a/src/assets/templates/agui-python-strands/dockerignore.template b/src/assets/templates/agui-python-strands/dockerignore.template deleted file mode 100644 index a0c4eb658..000000000 --- a/src/assets/templates/agui-python-strands/dockerignore.template +++ /dev/null @@ -1,27 +0,0 @@ -# Python -__pycache__/ -*.py[cod] -*.egg-info/ -.venv/ -dist/ -build/ - -# IDE -.vscode/ -.idea/ - -# Testing -.pytest_cache/ -.coverage -htmlcov/ - -# Secrets and environment files -.env -.env.* - -# Version control -.git/ - -# AgentCore build artifacts -.agentcore/artifacts/ -*.zip diff --git a/src/assets/templates/agui-python-strands/main.py b/src/assets/templates/agui-python-strands/main.py index 7b4f12495..d4a1d3122 100644 --- a/src/assets/templates/agui-python-strands/main.py +++ b/src/assets/templates/agui-python-strands/main.py @@ -4,9 +4,7 @@ from strands import Agent, tool from ag_ui_strands import StrandsAgent, StrandsAgentConfig, create_strands_app from model.load import load_model -{{#if hasMemory}} from memory.session import get_memory_session_manager -{{/if}} @tool @@ -21,7 +19,6 @@ def add_numbers(a: int, b: int) -> int: tools=[add_numbers], ) -{{#if hasMemory}} # The AG-UI protocol carries a per-conversation thread_id; each thread gets its # own session manager so history is scoped to the conversation. Returns None # (in-process history only) until the deployed MEMORY_ID env var is set. @@ -30,9 +27,6 @@ def session_manager_provider(input_data): config = StrandsAgentConfig(session_manager_provider=session_manager_provider) -{{else}} -config = StrandsAgentConfig() -{{/if}} agui_agent = StrandsAgent( agent=agent, name="{{ name }}", description="A helpful assistant", config=config diff --git a/src/assets/templates/agui-python-strands/memory/session.py b/src/assets/templates/agui-python-strands/memory/session.py index 20e105674..6372a2f68 100644 --- a/src/assets/templates/agui-python-strands/memory/session.py +++ b/src/assets/templates/agui-python-strands/memory/session.py @@ -2,7 +2,7 @@ import uuid from typing import Optional -from bedrock_agentcore.memory.integrations.strands.config import AgentCoreMemoryConfig{{#if memoryStrategies.length}}, RetrievalConfig{{/if}} +from bedrock_agentcore.memory.integrations.strands.config import AgentCoreMemoryConfig, RetrievalConfig from bedrock_agentcore.memory.integrations.strands.session_manager import AgentCoreMemorySessionManager MEMORY_ID = os.getenv("{{memoryEnvVarName}}") @@ -17,31 +17,19 @@ def get_memory_session_manager( session_id = session_id or uuid.uuid4().hex -{{#if memoryStrategies.length}} retrieval_config = { -{{#if (includes memoryStrategies "SEMANTIC")}} f"/users/{actor_id}/facts": RetrievalConfig(top_k=3, relevance_score=0.5), -{{/if}} -{{#if (includes memoryStrategies "USER_PREFERENCE")}} f"/users/{actor_id}/preferences": RetrievalConfig(top_k=3, relevance_score=0.5), -{{/if}} -{{#if (includes memoryStrategies "EPISODIC")}} f"/episodes/{actor_id}/{session_id}": RetrievalConfig(top_k=5, relevance_score=0.5), -{{/if}} -{{#if (includes memoryStrategies "SUMMARIZATION")}} f"/summaries/{actor_id}": RetrievalConfig(top_k=3, relevance_score=0.5), -{{/if}} } -{{/if}} return AgentCoreMemorySessionManager( AgentCoreMemoryConfig( memory_id=MEMORY_ID, session_id=session_id, actor_id=actor_id, -{{#if memoryStrategies.length}} retrieval_config=retrieval_config, -{{/if}} ), REGION, ) diff --git a/src/assets/templates/mcp-python-fastmcp/Dockerfile.template b/src/assets/templates/mcp-python-fastmcp/Dockerfile.template deleted file mode 100644 index cb3569eff..000000000 --- a/src/assets/templates/mcp-python-fastmcp/Dockerfile.template +++ /dev/null @@ -1,40 +0,0 @@ -FROM public.ecr.aws/docker/library/python:3.12-slim-trixie - -RUN pip install --no-cache-dir uv - -ARG UV_DEFAULT_INDEX -ARG UV_INDEX - -WORKDIR /app - -ENV UV_SYSTEM_PYTHON=1 \ - UV_COMPILE_BYTECODE=1 \ - UV_NO_PROGRESS=1 \ - PYTHONUNBUFFERED=1 \ - DOCKER_CONTAINER=1 \ - UV_DEFAULT_INDEX=${UV_DEFAULT_INDEX} \ - UV_INDEX=${UV_INDEX} \ - PATH="/app/.venv/bin:$PATH" - -RUN useradd -m -u 1000 bedrock_agentcore - -COPY pyproject.toml uv.lock ./ -RUN uv sync --frozen --no-dev --no-install-project - -COPY --chown=bedrock_agentcore:bedrock_agentcore . . -RUN uv sync --frozen --no-dev - -USER bedrock_agentcore - -# AgentCore Runtime service contract ports -# https://docs.aws.amazon.com/bedrock-agentcore/latest/devguide/runtime-service-contract.html -# 8080: HTTP Mode -# 8000: MCP Mode -# 9000: A2A Mode -EXPOSE 8080 8000 9000 - -{{#if enableOtel}} -CMD ["opentelemetry-instrument", "python", "-m", "{{entrypoint}}"] -{{else}} -CMD ["python", "-m", "{{entrypoint}}"] -{{/if}} diff --git a/src/assets/templates/mcp-python-fastmcp/dockerignore.template b/src/assets/templates/mcp-python-fastmcp/dockerignore.template deleted file mode 100644 index a0c4eb658..000000000 --- a/src/assets/templates/mcp-python-fastmcp/dockerignore.template +++ /dev/null @@ -1,27 +0,0 @@ -# Python -__pycache__/ -*.py[cod] -*.egg-info/ -.venv/ -dist/ -build/ - -# IDE -.vscode/ -.idea/ - -# Testing -.pytest_cache/ -.coverage -htmlcov/ - -# Secrets and environment files -.env -.env.* - -# Version control -.git/ - -# AgentCore build artifacts -.agentcore/artifacts/ -*.zip diff --git a/src/core/project/__snapshots__/manager.test.ts.snap b/src/core/project/__snapshots__/manager.test.ts.snap index 020a8bab0..d0b142fe1 100644 --- a/src/core/project/__snapshots__/manager.test.ts.snap +++ b/src/core/project/__snapshots__/manager.test.ts.snap @@ -17,9 +17,9 @@ exports[`FsProjectManager.create scaffolds the expected file tree into a fresh d "agentcore/cdk/package.json", "agentcore/cdk/test/cdk.test.ts", "agentcore/cdk/tsconfig.json", - "app/agent_python/README.md", - "app/agent_python/main.py", - "app/agent_python/pyproject.toml", + "app/agent_python_minimal/README.md", + "app/agent_python_minimal/main.py", + "app/agent_python_minimal/pyproject.toml", ] `; diff --git a/src/core/project/manager.export.test.ts b/src/core/project/manager.export.test.ts index 6ba6099f7..8bac9bd0b 100644 --- a/src/core/project/manager.export.test.ts +++ b/src/core/project/manager.export.test.ts @@ -61,7 +61,7 @@ async function projectWithHarness( name: "orders", skipInstall: true, skipGit: true, - scaffoldRuntimeInput: resolveRuntimeTemplateShortcut("agent-python"), + scaffoldRuntimeInput: resolveRuntimeTemplateShortcut("agent-python-minimal"), }), ); project = await drain( diff --git a/src/core/project/manager.test.ts b/src/core/project/manager.test.ts index cacf8208a..354407e4d 100644 --- a/src/core/project/manager.test.ts +++ b/src/core/project/manager.test.ts @@ -25,20 +25,13 @@ import { import { createSilentLogger, TestIdentityClient } from "../../testing"; import type { DeployBackendInput, ProjectBackend } from "./backends/types"; -const AGENT_PYTHON = resolveRuntimeTemplateShortcut("agent-python"); -const AGENT_PYTHON_CONTAINER = resolveRuntimeTemplateShortcut("agent-python", { - build: "Container", -}); +const AGENT_PYTHON = resolveRuntimeTemplateShortcut("agent-python-minimal"); const AGENT_PYTHON_STRANDS = resolveRuntimeTemplateShortcut("agent-python-strands"); +const AGENT_PYTHON_STRANDS_CONTAINER = resolveRuntimeTemplateShortcut( + "agent-python-strands-container", +); const AGENT_TYPESCRIPT_STRANDS = resolveRuntimeTemplateShortcut("agent-typescript-strands"); const A2A_PYTHON_STRANDS = resolveRuntimeTemplateShortcut("a2a-python-strands"); -const A2A_PYTHON_STRANDS_CONTAINER = resolveRuntimeTemplateShortcut("a2a-python-strands", { - build: "Container", -}); -const AGUI_PYTHON_STRANDS = resolveRuntimeTemplateShortcut("agui-python-strands"); -const AGUI_PYTHON_STRANDS_CONTAINER = resolveRuntimeTemplateShortcut("agui-python-strands", { - build: "Container", -}); const originalCwd = process.cwd(); const tempDirectories: string[] = []; @@ -196,77 +189,6 @@ describe("FsProjectManager.create", () => { expect(spec.memories).toMatchObject([{ name: "a2a_python_strandsMemory" }]); }); - test("scaffolds the Strands A2A runtime as a container with --build Container", async () => { - const directory = await inTempDirectory(); - await runCreate(manager().manager, { - name: "example", - scaffoldRuntimeInput: A2A_PYTHON_STRANDS_CONTAINER, - }); - - const appDir = join(directory, "example", "app", "a2a_python_strands"); - expect(await Bun.file(join(appDir, "Dockerfile")).exists()).toBe(true); - expect(await Bun.file(join(appDir, ".dockerignore")).exists()).toBe(true); - - const spec = await Bun.file(join(directory, "example", "agentcore", "agentcore.json")).json(); - expect(spec.runtimes[0]).toMatchObject({ - build: "Container", - dockerfile: "Dockerfile", - protocol: "A2A", - }); - }); - - test("snapshots the Strands AG-UI project manifest and runtime spec", async () => { - const directory = await inTempDirectory(); - await runCreate(manager().manager, { - name: "example", - scaffoldRuntimeInput: AGUI_PYTHON_STRANDS, - }); - - const projectRoot = join(directory, "example"); - const spec = await Bun.file(join(projectRoot, "agentcore", "agentcore.json")).json(); - expect({ - manifest: await projectManifest(projectRoot), - runtimes: spec.runtimes, - memories: spec.memories, - }).toMatchSnapshot(); - }); - - test("scaffolds the Strands AG-UI runtime with the AGUI protocol", async () => { - const directory = await inTempDirectory(); - await runCreate(manager().manager, { - name: "example", - scaffoldRuntimeInput: AGUI_PYTHON_STRANDS, - }); - - const spec = await Bun.file(join(directory, "example", "agentcore", "agentcore.json")).json(); - expect(spec.runtimes[0]).toMatchObject({ - name: "agui_python_strands", - build: "CodeZip", - protocol: "AGUI", - entrypoint: "main.py", - }); - expect(spec.memories).toMatchObject([{ name: "agui_python_strandsMemory" }]); - }); - - test("scaffolds the Strands AG-UI runtime as a container with --build Container", async () => { - const directory = await inTempDirectory(); - await runCreate(manager().manager, { - name: "example", - scaffoldRuntimeInput: AGUI_PYTHON_STRANDS_CONTAINER, - }); - - const appDir = join(directory, "example", "app", "agui_python_strands"); - expect(await Bun.file(join(appDir, "Dockerfile")).exists()).toBe(true); - expect(await Bun.file(join(appDir, ".dockerignore")).exists()).toBe(true); - - const spec = await Bun.file(join(directory, "example", "agentcore", "agentcore.json")).json(); - expect(spec.runtimes[0]).toMatchObject({ - build: "Container", - dockerfile: "Dockerfile", - protocol: "AGUI", - }); - }); - test("writes a deploy-ready agentcore.json registering the template agent", async () => { const directory = await inTempDirectory(); await runCreate(manager().manager, { @@ -279,10 +201,10 @@ describe("FsProjectManager.create", () => { expect(spec.name).toBe("example"); expect(spec.runtimes).toEqual([ { - name: "agent_python", + name: "agent_python_minimal", build: "CodeZip", entrypoint: "main.py", - codeLocation: "app/agent_python", + codeLocation: "app/agent_python_minimal", runtimeVersion: "PYTHON_3_14", }, ]); @@ -293,10 +215,10 @@ describe("FsProjectManager.create", () => { const directory = await inTempDirectory(); await runCreate(manager().manager, { name: "example", - scaffoldRuntimeInput: AGENT_PYTHON_CONTAINER, + scaffoldRuntimeInput: AGENT_PYTHON_STRANDS_CONTAINER, }); - const appDir = join(directory, "example", "app", "agent_python"); + const appDir = join(directory, "example", "app", "agent_python_strands_container"); // dockerignore.template must render to .dockerignore (the fsTree regex fix). expect(await Bun.file(join(appDir, ".dockerignore")).exists()).toBe(true); expect(await Bun.file(join(appDir, "dockerignore.template")).exists()).toBe(false); @@ -351,7 +273,7 @@ describe("FsProjectManager.create", () => { command: ["npm", "install", "--loglevel=http"], cwd: join(projectRoot, "agentcore", "cdk"), }, - { command: ["uv", "sync"], cwd: join(projectRoot, "app", "agent_python") }, + { command: ["uv", "sync"], cwd: join(projectRoot, "app", "agent_python_minimal") }, { command: ["git", "init"], cwd: projectRoot }, ]); }); @@ -425,15 +347,9 @@ describe("FsProjectManager.create", () => { test.each([ [ "Python", - resolveRuntimeTemplateShortcut("agent-python-strands", { build: "Container" }), + resolveRuntimeTemplateShortcut("agent-python-strands-container"), ["uv", "lock"], - "agent_python_strands", - ], - [ - "TypeScript", - resolveRuntimeTemplateShortcut("agent-typescript-strands", { build: "Container" }), - ["npm", "install", "--package-lock-only"], - "agent_typescript_strands", + "agent_python_strands_container", ], ])( "skipInstall still generates the container lockfile for %s", @@ -1005,10 +921,10 @@ describe("FsProjectManager.resolve", () => { version: 1, runtimes: [ { - name: "agent_python", + name: "agent_python_minimal", build: "CodeZip", entrypoint: "main.py", - codeLocation: "app/agent_python", + codeLocation: "app/agent_python_minimal", }, ], }), diff --git a/src/core/project/templates/runtime.ts b/src/core/project/templates/runtime.ts index 0748fd356..33f37bfa2 100644 --- a/src/core/project/templates/runtime.ts +++ b/src/core/project/templates/runtime.ts @@ -118,24 +118,10 @@ const importBedrockAgentResolver = () => async (input: RuntimeResourceConfig) => const getTemplateResolvers = (assetSource: AssetSource, templateRenderer: TemplateRenderer) => ({ [buildResolverKey("none", "Python", "HTTP")]: async (input: RuntimeResourceConfig) => { - const { modelProvider } = input.scaffoldRuntimeInput; - if (modelProvider !== undefined && modelProvider !== "Bedrock") - throw new InputValidationError( - "the agent-python template only supports the Bedrock model provider", - ); - if (input.scaffoldRuntimeInput.memory !== undefined) - throw new InputValidationError(`memory is not supported with the agent-python template`); - const isContainer = input.scaffoldRuntimeInput.build === "Container"; const tree = await FsTreeNode.fromAssetSource( { assetSource }, - { assetDir: "templates/agent-python" }, - { - rootDirName: input.name, - filter: (name) => { - if (name === "Dockerfile" || name === ".dockerignore") return isContainer; - return true; - }, - }, + { assetDir: "templates/agent-python-minimal" }, + { rootDirName: input.name }, ); return { tree, spec: { runtimes: [buildRuntimeSpec(input)] } }; }, @@ -145,10 +131,8 @@ const getTemplateResolvers = (assetSource: AssetSource, templateRenderer: Templa const context = { name: toPythonPackageName(input.name), modelProvider: input.scaffoldRuntimeInput.modelProvider ?? "Bedrock", - hasMemory: memory !== undefined, // the CDK injects this env var corresponding to the actual ID once its resolved on deployment. memoryEnvVarName: memory ? `MEMORY_${memory.name.toUpperCase()}_ID` : undefined, - memoryStrategies: memory?.strategies.map(({ type }) => type) ?? [], ...modelScaffold.templateRenderContext, enableOtel: true, // The strands template's entrypoint is fixed to main.py; the container Dockerfile launches it as the `main` module. @@ -183,31 +167,14 @@ const getTemplateResolvers = (assetSource: AssetSource, templateRenderer: Templa [buildResolverKey("strands", "TypeScript", "HTTP")]: async (input: RuntimeResourceConfig) => { if (input.protocol !== undefined && input.protocol !== "HTTP") throw new InputValidationError("the agent-typescript-strands template only supports HTTP"); - if (input.scaffoldRuntimeInput.modelProvider === "LiteLLM") - throw new InputValidationError( - "the agent-typescript-strands template does not support the LiteLLM model provider", - ); - const memory = input.scaffoldRuntimeInput.memory; - // The TypeScript strands SDK's createAgentCoreMemoryStores requires at least one - // namespace, so short-term-only memory (no long-term strategies) is unsupported. - // https://github.com/aws/bedrock-agentcore-sdk-typescript/blob/v0.3.0/src/memory/integrations/strands/factory.ts#L130-L133 - if (memory !== undefined && memory.strategies.length === 0) - throw new InputValidationError( - "the agent-typescript-strands template does not support short-term-only memory; add long-term strategies or use --memory none", - ); - const modelScaffold = resolveModelProviderScaffold(input); const context = { name: toNpmPackageName(input.name), - modelProvider: input.scaffoldRuntimeInput.modelProvider ?? "Bedrock", - hasMemory: memory !== undefined, // the CDK injects this env var corresponding to the actual ID once its resolved on deployment. memoryEnvVarName: memory ? `MEMORY_${memory.name.toUpperCase()}_ID` : undefined, - memoryStrategies: memory?.strategies.map(({ type }) => type) ?? [], ...modelScaffold.templateRenderContext, }; - const isContainer = input.scaffoldRuntimeInput.build === "Container"; const tree = await FsTreeNode.fromAssetSource( { assetSource }, { assetDir: "templates/agent-typescript-strands" }, @@ -216,7 +183,6 @@ const getTemplateResolvers = (assetSource: AssetSource, templateRenderer: Templa transformContent: (raw) => templateRenderer.render(raw, context), filter: (name, isDir) => { if (isDir && name === "memory") return memory !== undefined; - if (name === "Dockerfile" || name === ".dockerignore") return isContainer; return true; }, }, @@ -264,17 +230,12 @@ const getTemplateResolvers = (assetSource: AssetSource, templateRenderer: Templa enableOtel: true, entrypoint: "main", }; - const isContainer = input.scaffoldRuntimeInput.build === "Container"; const tree = await FsTreeNode.fromAssetSource( { assetSource }, { assetDir: "templates/mcp-python-fastmcp" }, { rootDirName: input.name, transformContent: (raw) => templateRenderer.render(raw, context), - filter: (name) => { - if (name === "Dockerfile" || name === ".dockerignore") return isContainer; - return true; - }, }, ); return { @@ -301,11 +262,8 @@ const getTemplateResolvers = (assetSource: AssetSource, templateRenderer: Templa const modelScaffold = resolveModelProviderScaffold(input); const context = { name: toPythonPackageName(input.name), - modelProvider: input.scaffoldRuntimeInput.modelProvider ?? "Bedrock", - hasMemory: memory !== undefined, // the CDK injects this env var corresponding to the actual ID once its resolved on deployment. memoryEnvVarName: memory ? `MEMORY_${memory.name.toUpperCase()}_ID` : undefined, - memoryStrategies: memory?.strategies.map(({ type }) => type) ?? [], ...modelScaffold.templateRenderContext, sessionStorageMountPath, efsMounts, @@ -317,7 +275,6 @@ const getTemplateResolvers = (assetSource: AssetSource, templateRenderer: Templa enableOtel: true, entrypoint: "main", }; - const isContainer = input.scaffoldRuntimeInput.build === "Container"; const tree = await FsTreeNode.fromAssetSource( { assetSource }, { assetDir: "templates/a2a-python-strands" }, @@ -326,7 +283,6 @@ const getTemplateResolvers = (assetSource: AssetSource, templateRenderer: Templa transformContent: (raw) => templateRenderer.render(raw, context), filter: (name, isDir) => { if (isDir && name === "memory") return memory !== undefined; - if (name === "Dockerfile" || name === ".dockerignore") return isContainer; return true; }, }, @@ -347,17 +303,13 @@ const getTemplateResolvers = (assetSource: AssetSource, templateRenderer: Templa const memory = input.scaffoldRuntimeInput.memory; const context = { name: toPythonPackageName(input.name), - hasMemory: memory !== undefined, // the CDK injects this env var corresponding to the actual ID once its resolved on deployment. memoryEnvVarName: memory ? `MEMORY_${memory.name.toUpperCase()}_ID` : undefined, - memoryStrategies: memory?.strategies.map(({ type }) => type) ?? [], - // The AgentCore Runtime requires OTEL dependencies to be present; the - // container launches main.py as the `main` module under - // opentelemetry-instrument, and the AG-UI app binds uvicorn on port 8080. + // The AgentCore Runtime requires OTEL dependencies to be present; the AG-UI + // app binds uvicorn on port 8080 under opentelemetry-instrument. enableOtel: true, entrypoint: "main", }; - const isContainer = input.scaffoldRuntimeInput.build === "Container"; const tree = await FsTreeNode.fromAssetSource( { assetSource }, { assetDir: "templates/agui-python-strands" }, @@ -366,7 +318,6 @@ const getTemplateResolvers = (assetSource: AssetSource, templateRenderer: Templa transformContent: (raw) => templateRenderer.render(raw, context), filter: (name, isDir) => { if (isDir && name === "memory") return memory !== undefined; - if (name === "Dockerfile" || name === ".dockerignore") return isContainer; return true; }, }, diff --git a/src/handlers/project/add/gateway-target/index.test.ts b/src/handlers/project/add/gateway-target/index.test.ts index 747b8410c..33b231566 100644 --- a/src/handlers/project/add/gateway-target/index.test.ts +++ b/src/handlers/project/add/gateway-target/index.test.ts @@ -58,7 +58,7 @@ describe("project add gateway-target", () => { "--name", "runtime", "--runtime", - "agent_python", + "agent_python_minimal", "--runtime-endpoint", "DEFAULT", ]); @@ -73,7 +73,7 @@ describe("project add gateway-target", () => { { name: "runtime", targetType: "httpRuntime", - httpRuntime: { runtime: "agent_python", runtimeEndpoint: "DEFAULT" }, + httpRuntime: { runtime: "agent_python_minimal", runtimeEndpoint: "DEFAULT" }, }, ]); }); @@ -253,7 +253,7 @@ describe("project add gateway-target", () => { ["no Target mode", ["--gateway", "tools", "--name", "target"], "specify exactly one"], [ "multiple Target modes", - [...endpointFlags(), "--runtime", "agent_python"], + [...endpointFlags(), "--runtime", "agent_python_minimal"], "specify exactly one", ], [ diff --git a/src/handlers/project/add/gateway-test-support.ts b/src/handlers/project/add/gateway-test-support.ts index e60f9976c..c0a6e0001 100644 --- a/src/handlers/project/add/gateway-test-support.ts +++ b/src/handlers/project/add/gateway-test-support.ts @@ -45,7 +45,7 @@ export function createGatewayProjectTestHarness(directoryPrefix: string) { "--name", name, "--template", - "agent-python", + "agent-python-minimal", "--skip-install", "--skip-git", ]); diff --git a/src/handlers/project/add/online-eval/index.test.ts b/src/handlers/project/add/online-eval/index.test.ts index aa7154fe2..97dc789d9 100644 --- a/src/handlers/project/add/online-eval/index.test.ts +++ b/src/handlers/project/add/online-eval/index.test.ts @@ -47,7 +47,7 @@ async function inProject(name = "TestProject"): Promise { "--name", name, "--template", - "agent-python", + "agent-python-minimal", "--skip-install", "--skip-git", ]); @@ -64,13 +64,13 @@ describe("project add online-eval", () => { "--name", "x", "--agent", - "agent_python", + "agent_python_minimal", "--evaluator", "Builtin.Correctness", "--sampling-rate", "50", ], - { agent: "agent_python", evaluators: ["Builtin.Correctness"], samplingRate: 50 }, + { agent: "agent_python_minimal", evaluators: ["Builtin.Correctness"], samplingRate: 50 }, ], [ "custom log-group source", @@ -92,7 +92,7 @@ describe("project add online-eval", () => { "--name", "x", "--agent", - "agent_python", + "agent_python_minimal", "--endpoint", "PROD", "--evaluator", @@ -100,7 +100,7 @@ describe("project add online-eval", () => { "--sampling-rate", "10", ], - { agent: "agent_python", endpoint: "PROD" }, + { agent: "agent_python_minimal", endpoint: "PROD" }, ], [ "service-name filter on a custom source", @@ -140,7 +140,7 @@ describe("project add online-eval", () => { "--name", "x", "--agent", - "agent_python", + "agent_python_minimal", "--evaluator", "Builtin.Correctness", "--sampling-rate", diff --git a/src/handlers/project/add/online-insight/index.test.ts b/src/handlers/project/add/online-insight/index.test.ts index 89551811f..dce9ca725 100644 --- a/src/handlers/project/add/online-insight/index.test.ts +++ b/src/handlers/project/add/online-insight/index.test.ts @@ -47,7 +47,7 @@ async function inProject(name = "TestProject"): Promise { "--name", name, "--template", - "agent-python", + "agent-python-minimal", "--skip-install", "--skip-git", ]); @@ -62,8 +62,17 @@ describe("project add online-insight", () => { test.each<[string, string[], Record]>([ [ "minimal — agent source", - ["--name", "x", "--agent", "agent_python", "--insight", INSIGHT, "--sampling-rate", "50"], - { agent: "agent_python", insights: [INSIGHT], samplingRate: 50 }, + [ + "--name", + "x", + "--agent", + "agent_python_minimal", + "--insight", + INSIGHT, + "--sampling-rate", + "50", + ], + { agent: "agent_python_minimal", insights: [INSIGHT], samplingRate: 50 }, ], [ "custom log-group source", @@ -85,7 +94,7 @@ describe("project add online-insight", () => { "--name", "x", "--agent", - "agent_python", + "agent_python_minimal", "--endpoint", "PROD", "--insight", @@ -93,7 +102,7 @@ describe("project add online-insight", () => { "--sampling-rate", "10", ], - { agent: "agent_python", endpoint: "PROD" }, + { agent: "agent_python_minimal", endpoint: "PROD" }, ], [ "clustering frequencies", @@ -101,7 +110,7 @@ describe("project add online-insight", () => { "--name", "x", "--agent", - "agent_python", + "agent_python_minimal", "--insight", INSIGHT, "--sampling-rate", @@ -134,7 +143,7 @@ describe("project add online-insight", () => { "--name", "x", "--agent", - "agent_python", + "agent_python_minimal", "--insight", INSIGHT, "--sampling-rate", @@ -163,7 +172,7 @@ describe("project add online-insight", () => { "--name", "x", "--agent", - "agent_python", + "agent_python_minimal", "--insight", INSIGHT, "--sampling-rate", @@ -190,7 +199,7 @@ describe("project add online-insight", () => { "--name", "x", "--agent", - "agent_python", + "agent_python_minimal", "--insight", INSIGHT, "--sampling-rate", diff --git a/src/handlers/project/add/payment-test-support.ts b/src/handlers/project/add/payment-test-support.ts index 0e2322802..4ec8be70c 100644 --- a/src/handlers/project/add/payment-test-support.ts +++ b/src/handlers/project/add/payment-test-support.ts @@ -33,7 +33,7 @@ export function createPaymentProjectTestHarness(directoryPrefix: string) { "--name", name, "--template", - "agent-python", + "agent-python-minimal", "--skip-install", "--skip-git", ]); diff --git a/src/handlers/project/add/runtime/index.test.ts b/src/handlers/project/add/runtime/index.test.ts index 836d03efa..56397610f 100644 --- a/src/handlers/project/add/runtime/index.test.ts +++ b/src/handlers/project/add/runtime/index.test.ts @@ -79,20 +79,7 @@ function translatedImportPlan( } describe("project add runtime", () => { - const template = ["--template", "agent-python"]; - - const allScaffoldingFlags = [ - "--build", - "CodeZip", - "--language", - "Python", - "--framework", - "none", - "--model-provider", - "Bedrock", - "--memory", - "none", - ]; + const template = ["--template", "agent-python-minimal"]; const allInfrastructureFlags = [ "--description", @@ -122,11 +109,7 @@ describe("project add runtime", () => { ]; const expectedSpecByLabel: Record> = { - "template overrides to Container": { - build: "Container", - dockerfile: "Dockerfile", - }, - "strands template overrides to Container": { + "strands -container template": { build: "Container", dockerfile: "Dockerfile", }, @@ -134,37 +117,14 @@ describe("project add runtime", () => { build: "CodeZip", protocol: "MCP", }, - "mcp-python-fastmcp overrides to Container": { - build: "Container", - dockerfile: "Dockerfile", - protocol: "MCP", - }, - "custom MCP runtime": { - build: "CodeZip", - protocol: "MCP", - }, "a2a-python-strands template preset": { build: "CodeZip", protocol: "A2A", }, - "a2a-python-strands overrides to Container": { - build: "Container", - dockerfile: "Dockerfile", - protocol: "A2A", - }, - "custom A2A runtime": { - build: "CodeZip", - protocol: "A2A", - }, "agui-python-strands template preset": { build: "CodeZip", protocol: "AGUI", }, - "agui-python-strands overrides to Container": { - build: "Container", - dockerfile: "Dockerfile", - protocol: "AGUI", - }, "all infrastructure flags": { description: "Configured runtime", executionRoleArn: "arn:aws:iam::123456789012:role/MyRole", @@ -192,170 +152,49 @@ describe("project add runtime", () => { }, }; + const mounts = [ + "--network-mode", + "VPC", + "--network-config", + '{"subnets":["subnet-0123456789abcdef0"],"securityGroups":["sg-0123456789abcdef0"]}', + "--filesystem-configurations", + '[{"sessionStorage":{"mountPath":"/mnt/session"}},{"efsAccessPoint":{"accessPointArn":"arn:aws:elasticfilesystem:us-east-1:123456789012:access-point/fsap-0123456789abcdef0","mountPath":"/mnt/efs"}},{"s3FilesAccessPoint":{"accessPointArn":"arn:aws:s3files:us-east-1:123456789012:file-system/fs-0123456789abcdef01/access-point/fsap-0123456789abcdef1","mountPath":"/mnt/s3"}}]', + ]; + test.each<[string, string[]]>([ - ["template preset", ["--name", "my_agent", ...template]], + ["default (no template) scaffolds agent-python-minimal", ["--name", "my_agent"]], + ["agent-python-minimal template preset", ["--name", "my_agent", ...template]], [ "agent-python-strands template preset", ["--name", "my_agent", "--template", "agent-python-strands"], ], [ - "template overrides to Container", - [ - "--name", - "my_agent", - "--template", - "agent-python", - "--build", - "Container", - "--model-provider", - "Bedrock", - "--memory", - "none", - ], - ], - [ - "strands template overrides to Container", - ["--name", "my_agent", "--template", "agent-python-strands", "--build", "Container"], + "strands -container template", + ["--name", "my_agent", "--template", "agent-python-strands-container"], ], [ "mcp-python-fastmcp template preset", ["--name", "my_mcp", "--template", "mcp-python-fastmcp"], ], - [ - "mcp-python-fastmcp overrides to Container", - ["--name", "my_mcp", "--template", "mcp-python-fastmcp", "--build", "Container"], - ], [ "a2a-python-strands template preset", ["--name", "my_a2a", "--template", "a2a-python-strands"], ], - [ - "a2a-python-strands overrides to Container", - ["--name", "my_a2a", "--template", "a2a-python-strands", "--build", "Container"], - ], - [ - "custom A2A runtime", - [ - "--name", - "a2a_custom", - "--build", - "CodeZip", - "--language", - "Python", - "--framework", - "strands", - "--protocol", - "A2A", - "--model-provider", - "Bedrock", - "--memory", - "none", - ], - ], [ "agui-python-strands template preset", ["--name", "my_agui", "--template", "agui-python-strands"], ], - [ - "agui-python-strands overrides to Container", - ["--name", "my_agui", "--template", "agui-python-strands", "--build", "Container"], - ], [ "agent-python-strands with session, EFS, and S3 mounts", - [ - "--name", - "fs_agent", - "--template", - "agent-python-strands", - "--network-mode", - "VPC", - "--network-config", - '{"subnets":["subnet-0123456789abcdef0"],"securityGroups":["sg-0123456789abcdef0"]}', - "--filesystem-configurations", - '[{"sessionStorage":{"mountPath":"/mnt/session"}},{"efsAccessPoint":{"accessPointArn":"arn:aws:elasticfilesystem:us-east-1:123456789012:access-point/fsap-0123456789abcdef0","mountPath":"/mnt/efs"}},{"s3FilesAccessPoint":{"accessPointArn":"arn:aws:s3files:us-east-1:123456789012:file-system/fs-0123456789abcdef01/access-point/fsap-0123456789abcdef1","mountPath":"/mnt/s3"}}]', - ], + ["--name", "fs_agent", "--template", "agent-python-strands", ...mounts], ], [ "mcp-python-fastmcp with session, EFS, and S3 mounts", - [ - "--name", - "fs_mcp", - "--template", - "mcp-python-fastmcp", - "--network-mode", - "VPC", - "--network-config", - '{"subnets":["subnet-0123456789abcdef0"],"securityGroups":["sg-0123456789abcdef0"]}', - "--filesystem-configurations", - '[{"sessionStorage":{"mountPath":"/mnt/session"}},{"efsAccessPoint":{"accessPointArn":"arn:aws:elasticfilesystem:us-east-1:123456789012:access-point/fsap-0123456789abcdef0","mountPath":"/mnt/efs"}},{"s3FilesAccessPoint":{"accessPointArn":"arn:aws:s3files:us-east-1:123456789012:file-system/fs-0123456789abcdef01/access-point/fsap-0123456789abcdef1","mountPath":"/mnt/s3"}}]', - ], + ["--name", "fs_mcp", "--template", "mcp-python-fastmcp", ...mounts], ], [ "a2a-python-strands with session, EFS, and S3 mounts", - [ - "--name", - "fs_a2a", - "--template", - "a2a-python-strands", - "--network-mode", - "VPC", - "--network-config", - '{"subnets":["subnet-0123456789abcdef0"],"securityGroups":["sg-0123456789abcdef0"]}', - "--filesystem-configurations", - '[{"sessionStorage":{"mountPath":"/mnt/session"}},{"efsAccessPoint":{"accessPointArn":"arn:aws:elasticfilesystem:us-east-1:123456789012:access-point/fsap-0123456789abcdef0","mountPath":"/mnt/efs"}},{"s3FilesAccessPoint":{"accessPointArn":"arn:aws:s3files:us-east-1:123456789012:file-system/fs-0123456789abcdef01/access-point/fsap-0123456789abcdef1","mountPath":"/mnt/s3"}}]', - ], - ], - [ - "custom MCP runtime", - [ - "--name", - "mcp_custom", - "--build", - "CodeZip", - "--language", - "Python", - "--framework", - "none", - "--protocol", - "MCP", - "--memory", - "none", - ], - ], - ["custom — all scaffolding flags", ["--name", "my_agent", ...allScaffoldingFlags]], - [ - "custom — framework strands", - [ - "--name", - "strands_agent", - "--build", - "CodeZip", - "--language", - "Python", - "--framework", - "strands", - "--model-provider", - "Bedrock", - "--memory", - "none", - ], - ], - [ - "custom — container build", - [ - "--name", - "my_agent", - "--build", - "Container", - "--language", - "Python", - "--framework", - "none", - "--model-provider", - "Bedrock", - "--memory", - "none", - ], + ["--name", "fs_a2a", "--template", "a2a-python-strands", ...mounts], ], ["description", ["--name", "my_agent", ...template, "--description", "A test agent"]], [ @@ -428,34 +267,6 @@ describe("project add runtime", () => { '[{"sessionStorage":{"mountPath":"/mnt/data"}}]', ], ], - [ - "filesystem-configurations — efsAccessPoint", - [ - "--name", - "my_agent", - ...template, - "--network-mode", - "VPC", - "--network-config", - '{"subnets":["subnet-0123456789abcdef0"],"securityGroups":["sg-0123456789abcdef0"]}', - "--filesystem-configurations", - '[{"efsAccessPoint":{"accessPointArn":"arn:aws:elasticfilesystem:us-east-1:123456789012:access-point/fsap-0123456789abcdef0","mountPath":"/mnt/efs"}}]', - ], - ], - [ - "filesystem-configurations — s3FilesAccessPoint", - [ - "--name", - "my_agent", - ...template, - "--network-mode", - "VPC", - "--network-config", - '{"subnets":["subnet-0123456789abcdef0"],"securityGroups":["sg-0123456789abcdef0"]}', - "--filesystem-configurations", - '[{"s3FilesAccessPoint":{"accessPointArn":"arn:aws:s3files:us-east-1:123456789012:file-system/fs-0123456789abcdef01/access-point/fsap-0123456789abcdef1","mountPath":"/mnt/s3"}}]', - ], - ], ["tags", ["--name", "my_agent", ...template, "--tags", '{"team":"ml","env":"prod"}']], [ "additional-policies", @@ -480,8 +291,7 @@ describe("project add runtime", () => { const runtime = spec.runtimes.find((candidate: { name: string }) => candidate.name === name); expect(runtime).toMatchObject({ entrypoint: "main.py", ...expectedSpecByLabel[label] }); expect(await Bun.file(join(projectRoot, "app", name, "main.py")).exists()).toBe(true); - const buildFlagIndex = flags.indexOf("--build"); - const isContainer = buildFlagIndex >= 0 && flags[buildFlagIndex + 1] === "Container"; + const isContainer = flags.some((flag) => flag.endsWith("-container")); expect(runtime.runtimeVersion).toBe(isContainer ? undefined : "PYTHON_3_14"); expect(await Bun.file(join(projectRoot, "app", name, "Dockerfile")).exists()).toBe(isContainer); expect(await Bun.file(join(projectRoot, "app", name, ".dockerignore")).exists()).toBe( @@ -489,369 +299,63 @@ describe("project add runtime", () => { ); }); - test.each([ - ["default", [], ["SEMANTIC", "USER_PREFERENCE", "SUMMARIZATION", "EPISODIC"]], - ["none", ["--memory", "none"], []], - ["short", ["--memory", "shortTerm"], []], - [ - "longAndShortTerm", - ["--memory", "longAndShortTerm"], - ["SEMANTIC", "USER_PREFERENCE", "SUMMARIZATION", "EPISODIC"], - ], - ])("custom strands %s memory", async (_label, memoryFlags, expectedStrategies) => { + test.each<[string, string[]]>([ + ["agent-python-strands", ["SEMANTIC", "USER_PREFERENCE", "SUMMARIZATION", "EPISODIC"]], + ["a2a-python-strands", ["SEMANTIC", "USER_PREFERENCE", "SUMMARIZATION", "EPISODIC"]], + ["agent-python-minimal", []], + ["mcp-python-fastmcp", []], + ["agui-python-strands", ["SEMANTIC", "USER_PREFERENCE", "SUMMARIZATION", "EPISODIC"]], + ])("%s ships with its pre-configured memory", async (templateName, expectedStrategies) => { const projectRoot = await inProject(); - await run([ - "add", - "runtime", - "--name", - "my_agent", - "--build", - "CodeZip", - "--language", - "Python", - "--framework", - "strands", - "--model-provider", - "Bedrock", - ...memoryFlags, - ]); + await run(["add", "runtime", "--name", "my_agent", "--template", templateName]); const spec = await Bun.file(join(projectRoot, "agentcore", "agentcore.json")).json(); - const memory = spec.memories.find( + const memory = (spec.memories ?? []).find( (candidate: { name: string }) => candidate.name === "my_agentMemory", ); - if (memoryFlags.length > 1 && memoryFlags[1] === "none") { + if (expectedStrategies.length === 0) { expect(memory).toBeUndefined(); return; } - expect(memory).toMatchObject({ - name: "my_agentMemory", - eventExpiryDuration: 30, - }); + expect(memory).toMatchObject({ name: "my_agentMemory", eventExpiryDuration: 30 }); expect(memory.strategies.map(({ type }: { type: string }) => type)).toEqual(expectedStrategies); }); - test.each<[string, string[], string[]]>([ - [ - "template preset defaults to long and short-term memory", - ["--name", "my_a2a", "--template", "a2a-python-strands"], - ["SEMANTIC", "USER_PREFERENCE", "SUMMARIZATION", "EPISODIC"], - ], - [ - "template preset with --memory none", - ["--name", "my_a2a", "--template", "a2a-python-strands", "--memory", "none"], - [], - ], - ])("a2a-python-strands %s", async (_label, flags, expectedStrategies) => { + test("agent-typescript-strands scaffolds a TypeScript agent", async () => { const projectRoot = await inProject(); - await run(["add", "runtime", ...flags]); + await run(["add", "runtime", "--name", "my_agent", "--template", "agent-typescript-strands"]); const spec = await Bun.file(join(projectRoot, "agentcore", "agentcore.json")).json(); - const memory = (spec.memories ?? []).find( - (candidate: { name: string }) => candidate.name === "my_a2aMemory", + const runtime = spec.runtimes.find( + (candidate: { name: string }) => candidate.name === "my_agent", ); - const memoryDir = join(projectRoot, "app", "my_a2a", "memory", "session.py"); - - if (expectedStrategies.length === 0) { - expect(memory).toBeUndefined(); - expect(await Bun.file(memoryDir).exists()).toBe(false); - return; - } - - expect(memory.strategies.map(({ type }: { type: string }) => type)).toEqual(expectedStrategies); - expect(await Bun.file(memoryDir).exists()).toBe(true); - }); - - test.each<[string, string[], string[]]>([ - [ - "template preset defaults to long and short-term memory", - ["--name", "my_agui", "--template", "agui-python-strands"], - ["SEMANTIC", "USER_PREFERENCE", "SUMMARIZATION", "EPISODIC"], - ], - [ - "template preset with --memory none", - ["--name", "my_agui", "--template", "agui-python-strands", "--memory", "none"], - [], - ], - ])("agui-python-strands %s", async (_label, flags, expectedStrategies) => { - const projectRoot = await inProject(); - await run(["add", "runtime", ...flags]); + expect(runtime).toMatchObject({ + entrypoint: "main.js", + build: "CodeZip", + runtimeVersion: "NODE_22", + }); + expect(await Bun.file(join(projectRoot, "app", "my_agent", "main.ts")).exists()).toBe(true); - const spec = await Bun.file(join(projectRoot, "agentcore", "agentcore.json")).json(); const memory = (spec.memories ?? []).find( - (candidate: { name: string }) => candidate.name === "my_aguiMemory", + (candidate: { name: string }) => candidate.name === "my_agentMemory", ); - const memoryDir = join(projectRoot, "app", "my_agui", "memory", "session.py"); - - if (expectedStrategies.length === 0) { - expect(memory).toBeUndefined(); - expect(await Bun.file(memoryDir).exists()).toBe(false); - return; - } - - expect(memory.strategies.map(({ type }: { type: string }) => type)).toEqual(expectedStrategies); - expect(await Bun.file(memoryDir).exists()).toBe(true); - }); - - test.each<[string, string[], string[]]>([ - [ - "template preset", - ["--name", "my_agent", "--template", "agent-typescript-strands"], - ["SEMANTIC", "USER_PREFERENCE", "SUMMARIZATION", "EPISODIC"], - ], - [ - "custom without memory", - [ - "--name", - "my_agent", - "--build", - "CodeZip", - "--language", - "TypeScript", - "--framework", - "strands", - "--model-provider", - "Bedrock", - "--memory", - "none", - ], - [], - ], - [ - "template overrides to Container", - ["--name", "my_agent", "--template", "agent-typescript-strands", "--build", "Container"], - ["SEMANTIC", "USER_PREFERENCE", "SUMMARIZATION", "EPISODIC"], - ], - ])( - "agent-typescript-strands %s scaffolds a TypeScript agent", - async (_label, flags, expectedStrategies) => { - const projectRoot = await inProject(); - await run(["add", "runtime", ...flags]); - - const buildFlagIndex = flags.indexOf("--build"); - const isContainer = buildFlagIndex >= 0 && flags[buildFlagIndex + 1] === "Container"; - - const spec = await Bun.file(join(projectRoot, "agentcore", "agentcore.json")).json(); - const runtime = spec.runtimes.find( - (candidate: { name: string }) => candidate.name === "my_agent", - ); - expect(runtime).toMatchObject({ - entrypoint: "main.js", - ...(isContainer - ? { build: "Container", dockerfile: "Dockerfile" } - : { build: "CodeZip", runtimeVersion: "NODE_22" }), - }); - expect(runtime.runtimeVersion).toBe(isContainer ? undefined : "NODE_22"); - expect(await Bun.file(join(projectRoot, "app", "my_agent", "Dockerfile")).exists()).toBe( - isContainer, - ); - expect(await Bun.file(join(projectRoot, "app", "my_agent", ".dockerignore")).exists()).toBe( - isContainer, - ); - - const memory = (spec.memories ?? []).find( - (candidate: { name: string }) => candidate.name === "my_agentMemory", - ); - const strategies = memory?.strategies.map(({ type }: { type: string }) => type) ?? []; - expect(strategies).toEqual(expectedStrategies); - }, - ); - - test.each<[string, string[]]>([ - ["missing --name", ["--template", "agent-python"]], - [ - "missing --build without --template", - [ - "--name", - "my_agent", - "--language", - "Python", - "--framework", - "none", - "--model-provider", - "Bedrock", - "--memory", - "none", - ], - ], - [ - "--protocol cannot override the agent-python-strands template", - ["--name", "my_agent", "--template", "agent-python-strands", "--protocol", "MCP"], - ], - [ - "TypeScript without a strands template has no resolver", - [ - "--name", - "my_agent", - "--build", - "CodeZip", - "--language", - "TypeScript", - "--framework", - "none", - "--model-provider", - "Bedrock", - ], - ], - [ - "agent-typescript-strands rejects short-term-only memory", - [ - "--name", - "my_agent", - "--build", - "CodeZip", - "--language", - "TypeScript", - "--framework", - "strands", - "--model-provider", - "Bedrock", - "--memory", - "shortTerm", - ], - ], - [ - "invalid JSON in --network-config", - ["--name", "my_agent", ...template, "--network-config", "{bad}"], - ], - [ - "--protocol cannot override the agent-python template", - ["--name", "my_agent", "--template", "agent-python", "--protocol", "MCP"], - ], - [ - "mcp-python-fastmcp does not support memory", - ["--name", "my_agent", "--template", "mcp-python-fastmcp", "--memory", "shortTerm"], - ], - [ - "--protocol alone requires --framework and --language", - ["--name", "my_agent", "--protocol", "MCP"], - ], - [ - "--protocol cannot override a template", - ["--name", "my_agent", "--template", "mcp-python-fastmcp", "--protocol", "MCP"], - ], - [ - "custom MCP runtime does not support memory", - [ - "--name", - "my_agent", - "--build", - "CodeZip", - "--language", - "Python", - "--framework", - "none", - "--protocol", - "MCP", - "--memory", - "shortTerm", - ], - ], - [ - "MCP runtime rejects a model provider", - [ - "--name", - "my_agent", - "--build", - "CodeZip", - "--language", - "Python", - "--framework", - "none", - "--protocol", - "MCP", - "--model-provider", - "Bedrock", - ], - ], - [ - "--memory shortTerm is not supported with --framework none", - [ - "--name", - "my_agent", - "--build", - "CodeZip", - "--language", - "Python", - "--framework", - "none", - "--model-provider", - "Bedrock", - "--memory", - "shortTerm", - ], - ], - [ - "--memory longAndShortTerm is not supported with --framework none", - [ - "--name", - "my_agent", - "--build", - "CodeZip", - "--language", - "Python", - "--framework", - "none", - "--model-provider", - "Bedrock", - "--memory", - "longAndShortTerm", - ], - ], - ["runtime names are limited in length", ["--name", "x".repeat(43)]], - ])("%s", async (_label, flags) => { - await inProject(); - await expect(run(["add", "runtime", ...flags])).rejects.toBeInstanceOf(InputValidationError); - }); - - test.each([ - ["language", "Python"], - ["framework", "none"], - ])("rejects --%s as a template override", async (flagName, value) => { - await inProject(); - await expect( - run([ - "add", - "runtime", - "--name", - "my_agent", - "--template", - "agent-python", - `--${flagName}`, - value, - ]), - ).rejects.toThrow(`--${flagName} cannot override a template`); - }); - - test("rejects an incompatible API-key template override", async () => { - const projectRoot = await inProject(); - const apiKeyPath = join(projectRoot, "api-key.txt"); - await Bun.write(apiKeyPath, "secret-key"); - - await expect( - run([ - "add", - "runtime", - "--name", - "my_agent", - "--template", - "agent-python", - "--api-key", - `file://${apiKeyPath}`, - ]), - ).rejects.toThrow(/API keys are not compatible with Bedrock model providers/); + expect(memory?.strategies.map(({ type }: { type: string }) => type)).toEqual([ + "SEMANTIC", + "USER_PREFERENCE", + "SUMMARIZATION", + "EPISODIC", + ]); }); - test.each<[string, string, string]>([ - ["anthropic", "Anthropic", "Python"], - ["OpenAI", "OpenAI", "Python"], - ["gemini", "Gemini", "Python"], - ["Anthropic", "Anthropic", "TypeScript"], + test.each<[string, string]>([ + ["anthropic", "Anthropic"], + ["OpenAI", "OpenAI"], + ["gemini", "Gemini"], ])( - "scaffolds a strands runtime for --model-provider %s (%s) with an API-key credential", - async (flagValue, provider, language) => { + "scaffolds agent-python-strands for --model-provider %s with an API-key credential", + async (flagValue, provider) => { const projectRoot = await inProject(); const apiKeyPath = join(projectRoot, "api-key.txt"); await Bun.write(apiKeyPath, "test-api-key"); @@ -861,12 +365,8 @@ describe("project add runtime", () => { "runtime", "--name", "my_agent", - "--build", - "CodeZip", - "--language", - language, - "--framework", - "strands", + "--template", + "agent-python-strands", "--model-provider", flagValue, "--api-key", @@ -887,35 +387,36 @@ describe("project add runtime", () => { }, ); - test.each<[string, string, string, boolean]>([ - ["Anthropic without an API key", "Anthropic", "strands", false], - ["OpenAI without an API key", "OpenAI", "strands", false], - ["Gemini without an API key", "Gemini", "strands", false], - ["a non-Bedrock provider on a provider-less template", "Anthropic", "none", true], - ["LiteLLM on the TypeScript template", "LiteLLM", "strands", false], - ])("rejects %s", async (_label, provider, framework, includeApiKey) => { - const projectRoot = await inProject(); - const apiKeyPath = join(projectRoot, "api-key.txt"); - await Bun.write(apiKeyPath, "test-api-key"); - - const language = provider === "LiteLLM" ? "TypeScript" : "Python"; - const flags = [ - "add", - "runtime", - "--name", - "my_agent", - "--build", - "CodeZip", - "--language", - language, - "--framework", - framework, - "--model-provider", - provider, - ]; - if (includeApiKey) flags.push("--api-key", `file://${apiKeyPath}`); + test.each<[string, string[]]>([ + ["missing --name", ["--template", "agent-python-minimal"]], + [ + "--model-provider is not valid with the a2a-python-strands template", + ["--name", "my_agent", "--template", "a2a-python-strands", "--model-provider", "Anthropic"], + ], + [ + "--api-key is not valid with the agent-python-minimal template", + ["--name", "my_agent", "--template", "agent-python-minimal", "--api-key", "secret-key"], + ], + [ + "--model-provider without a template requires agent-python-strands", + ["--name", "my_agent", "--model-provider", "Anthropic"], + ], + ["--framework requires --type import", ["--name", "my_agent", "--framework", "strands"]], + [ + "invalid JSON in --network-config", + ["--name", "my_agent", ...template, "--network-config", "{bad}"], + ], + ["runtime names are limited in length", ["--name", "x".repeat(43)]], + ])("%s", async (_label, flags) => { + await inProject(); + await expect(run(["add", "runtime", ...flags])).rejects.toBeInstanceOf(InputValidationError); + }); - await expect(run(flags)).rejects.toBeInstanceOf(InputValidationError); + test("rejects an unknown --template value", async () => { + await inProject(); + await expect( + run(["add", "runtime", "--name", "my_agent", "--template", "nonsense"]), + ).rejects.toThrow(); }); }); @@ -949,7 +450,7 @@ describe("project add runtime --type import", () => { agentId: "A1B2C3D4E5", agentAliasId: "TSTALIASID", framework: "strands", - memory: "none", + memory: "longAndShortTerm", }, ]); @@ -976,36 +477,27 @@ describe("project add runtime --type import", () => { expect(pyproject).toContain('name = "support-proxy"'); }); - test("supports LangGraph translation and target memory", async () => { + test("supports LangGraph translation", async () => { const projectRoot = await inProject(); const core = new TestCoreClient(); core.bedrockAgentImportPlans["A1B2C3D4E5/TSTALIASID"] = translatedImportPlan({ framework: "langgraph", }); - await run([...importArgs, "--framework", "langgraph", "--memory", "longAndShortTerm"], { - core, - }); + await run([...importArgs, "--framework", "langgraph"], { core }); expect(core.importedBedrockAgents[0]).toMatchObject({ framework: "langgraph", memory: "longAndShortTerm", }); const spec = await Bun.file(join(projectRoot, "agentcore", "agentcore.json")).json(); - expect(spec.memories[0]).toMatchObject({ - name: "support_proxyMemory", - strategies: expect.any(Array), - }); - }); - - test("rejects a non-HTTP protocol before importing the agent", async () => { - await inProject(); - const core = new TestCoreClient(); - - await expect(run([...importArgs, "--protocol", "MCP"], { core })).rejects.toThrow( - /only supports HTTP/, - ); - expect(core.importedBedrockAgents).toEqual([]); + expect(spec.memories).toMatchObject([{ name: "support_proxyMemory" }]); + expect(spec.memories[0].strategies.map(({ type }: { type: string }) => type)).toEqual([ + "SEMANTIC", + "USER_PREFERENCE", + "SUMMARIZATION", + "EPISODIC", + ]); }); test("documents required permissions instead of generating policies for a caller-owned role", async () => { @@ -1054,16 +546,13 @@ describe("project add runtime --type import", () => { ); }); - test("accepts translation flags and rejects incompatible scaffolding flags", async () => { + test("accepts translation flags and rejects a template", async () => { await inProject(); const core = new TestCoreClient(); core.bedrockAgentImportPlans["A1B2C3D4E5/TSTALIASID"] = translatedImportPlan(); await expect(run([...importArgs, "--framework", "strands"], { core })).resolves.toBeDefined(); - await expect(run([...importArgs, "--template", "agent-python"])).rejects.toThrow( + await expect(run([...importArgs, "--template", "agent-python-minimal"])).rejects.toThrow( /--template cannot be combined/, ); - await expect(run([...importArgs, "--build", "Container"])).rejects.toThrow( - /--build cannot be combined/, - ); }); }); diff --git a/src/handlers/project/add/runtime/index.ts b/src/handlers/project/add/runtime/index.ts index bea398c56..d62142a7e 100644 --- a/src/handlers/project/add/runtime/index.ts +++ b/src/handlers/project/add/runtime/index.ts @@ -3,22 +3,17 @@ import { createHandler, flag, ProjectKey } from "../../../../router"; import type { AddProjectResourceConfig } from "../types"; import { parseJsonFlag, parseTags } from "../../../utils"; import { InputValidationError } from "../../../../errors"; -import { type EnvVar, BuildTypeSchema } from "../../../../projectSchemas/runtime"; +import { type EnvVar } from "../../../../projectSchemas/runtime"; import { RuntimeAuthorizerTypeSchema } from "../../../../projectSchemas/auth"; import { NetworkModeSchema } from "../../../../projectSchemas/constants"; import { SourceResolver } from "../../../../io"; import { - LANGUAGE_VERSION_DEFAULTS, - MEMORY_SHORTCUT_NAMES, - MEMORY_SHORTCUTS, + RUNTIME_TEMPLATE_SHORTCUTS, RUNTIME_TEMPLATE_SHORTCUT_NAMES, + getDefaultMemorySpec, resolveRuntimeTemplateShortcut, } from "../../shortcuts"; -import { - ModelProviderSchema, - ScaffoldRuntimeInputSchema, - type ScaffoldRuntimeInput, -} from "../../types"; +import { ModelProviderSchema, type ScaffoldRuntimeInput } from "../../types"; import { RuntimeResourceConfigSchema, type ImportBedrockAgentInput } from "./types"; import { importScaffoldRuntimeInput, @@ -54,16 +49,10 @@ export const createAddRuntimeHandler = (config: AddProjectResourceConfig) => "a preset of flags for scaffolding the Runtime; compatible flags override preset values", z.enum(RUNTIME_TEMPLATE_SHORTCUT_NAMES).optional(), ), - flag("build", "build type: CodeZip or Container", BuildTypeSchema.optional()), - flag( - "language", - "target language for the scaffolded Runtime code", - z.enum(["Python", "TypeScript"]).optional(), - ), flag( "framework", - "agent framework: strands or none for create; strands or langgraph for import", - z.enum(["strands", "langgraph", "none"]).optional(), + "agent framework for an imported Bedrock Agent: strands or langgraph (requires --type import)", + z.enum(["strands", "langgraph"]).optional(), ), flag( "model-provider", @@ -76,11 +65,6 @@ export const createAddRuntimeHandler = (config: AddProjectResourceConfig) => z.string().optional(), { sensitive: true }, ), - flag( - "memory", - "memory option for the scaffolded Runtime", - z.enum(MEMORY_SHORTCUT_NAMES).optional(), - ), flag( "role-arn", "IAM role ARN that provides permissions for the Runtime", @@ -91,11 +75,6 @@ export const createAddRuntimeHandler = (config: AddProjectResourceConfig) => "additional IAM policy ARNs or policy document paths for the execution role", z.array(z.string()).optional(), ), - flag( - "protocol", - "server protocol: HTTP, MCP, or A2A", - z.enum(["HTTP", "MCP", "A2A"]).optional(), - ), flag( "network-mode", "network mode for the Runtime environment (PUBLIC or VPC)", @@ -134,55 +113,48 @@ export const createAddRuntimeHandler = (config: AddProjectResourceConfig) => if (!flags.name) throw new InputValidationError("required option '--name ' not specified"); - const scaffoldingFlags = [ - "build", - "language", - "framework", - "protocol", - "model-provider", - "api-key", - "memory", - ] as const; - const presentScaffoldingFlags = scaffoldingFlags.filter((f) => flags[f] !== undefined); + const isImport = flags["type"] === "import"; const isTemplate = flags["template"] !== undefined; - const lockedFlag = (["language", "framework", "protocol"] as const).find( - (flagName) => flags[flagName] !== undefined, - ); - if (isTemplate && lockedFlag) { - throw new InputValidationError(`--${lockedFlag} cannot override a template`); + const modelFlagsPresent = + flags["model-provider"] !== undefined || flags["api-key"] !== undefined; + + if (flags.framework !== undefined && !isImport) { + throw new InputValidationError("--framework requires --type import"); } - const isImport = flags["type"] === "import"; - const importIncompatibleFlags = ( - ["build", "language", "model-provider", "api-key"] as const - ).filter((flagName) => flags[flagName] !== undefined); - if (isImport && (isTemplate || importIncompatibleFlags.length > 0)) { - const offending = isTemplate ? "template" : importIncompatibleFlags[0]; - throw new InputValidationError( - `--type import translates a Bedrock Agent into Python CodeZip runtime code; ` + - `--${offending} cannot be combined with it`, + if (isImport) { + const importIncompatibleFlags = (["model-provider", "api-key"] as const).filter( + (flagName) => flags[flagName] !== undefined, ); + if (isTemplate || importIncompatibleFlags.length > 0) { + const offending = isTemplate ? "template" : importIncompatibleFlags[0]; + throw new InputValidationError( + `--type import translates a Bedrock Agent into Python CodeZip runtime code; ` + + `--${offending} cannot be combined with it`, + ); + } } if (!isImport && (flags["agent-id"] !== undefined || flags["agent-alias-id"] !== undefined)) { throw new InputValidationError("--agent-id and --agent-alias-id require --type import"); } - if (isImport && flags.framework === "none") { - throw new InputValidationError("--type import supports --framework strands or langgraph"); - } - if (!isImport && flags.framework === "langgraph") { - throw new InputValidationError("--framework langgraph requires --type import"); - } - if (isImport && flags.protocol !== undefined && flags.protocol !== "HTTP") { - throw new InputValidationError("an imported Bedrock Agent only supports HTTP"); - } - const isCustom = presentScaffoldingFlags.length > 0; + if (!isImport && modelFlagsPresent) { + if (!isTemplate) { + throw new InputValidationError( + "--model-provider and --api-key only apply to templates that support them", + ); + } + if (!RUNTIME_TEMPLATE_SHORTCUTS[flags.template!].supportsModelProviderOverride) { + throw new InputValidationError( + `--model-provider and --api-key are not valid with the ${flags.template} template`, + ); + } + } const source = new SourceResolver({ stdin: config.io.stdin }); const apiKey = await source.resolveSecret("api-key", flags["api-key"]); const runtimeName = flags.name; - const importMemory = flags.memory ?? "none"; let importBedrockAgent: ImportBedrockAgentInput | undefined; if (isImport) { @@ -193,7 +165,7 @@ export const createAddRuntimeHandler = (config: AddProjectResourceConfig) => agentId: flags["agent-id"], agentAliasId: flags["agent-alias-id"], framework: flags.framework === "langgraph" ? "langgraph" : "strands", - memory: importMemory, + memory: "longAndShortTerm", }); if (importBedrockAgent.notes.length > 0) { config.io.stderr.write( @@ -204,33 +176,15 @@ export const createAddRuntimeHandler = (config: AddProjectResourceConfig) => } } - const defaultMemory = flags.framework === "strands" ? "longAndShortTerm" : "none"; const scaffoldRuntimeInput: ScaffoldRuntimeInput = isImport - ? importScaffoldRuntimeInput(runtimeName, MEMORY_SHORTCUTS[importMemory](runtimeName)) + ? importScaffoldRuntimeInput(runtimeName, getDefaultMemorySpec(runtimeName)) : isTemplate ? resolveRuntimeTemplateShortcut(flags.template!, { runtimeName: flags.name, - build: flags.build, modelProvider: flags["model-provider"], apiKey, - memory: flags.memory, }) - : isCustom - ? parseScaffoldRuntimeInput({ - runtimeName, - build: flags.build, - language: flags.language, - framework: flags.framework === "langgraph" ? undefined : flags.framework, - protocol: flags.protocol, - modelProvider: flags["model-provider"], - apiKey, - memory: MEMORY_SHORTCUTS[flags.memory ?? defaultMemory](runtimeName), - runtimeVersion: - flags.build === "CodeZip" - ? LANGUAGE_VERSION_DEFAULTS[flags.language ?? "Python"] - : undefined, - }) - : resolveRuntimeTemplateShortcut("agent-python", { runtimeName: flags.name }); + : resolveRuntimeTemplateShortcut("agent-python-minimal", { runtimeName: flags.name }); const inputEnvironmentVariables = parseJsonFlag>( "environment-variables", @@ -250,7 +204,6 @@ export const createAddRuntimeHandler = (config: AddProjectResourceConfig) => "authorizer-configuration", flags["authorizer-configuration"], ), - protocol: flags["protocol"], requestHeaderAllowlist: flags["request-header-allowlist"], lifecycleConfiguration: parseJsonFlag( "lifecycle-configuration", @@ -284,9 +237,3 @@ export const createAddRuntimeHandler = (config: AddProjectResourceConfig) => function toEnvironmentVariables(envVars: Record | undefined): EnvVar[] { return envVars ? Object.entries(envVars).map(([name, value]) => ({ name, value })) : []; } - -function parseScaffoldRuntimeInput(input: Partial) { - const result = ScaffoldRuntimeInputSchema.safeParse(input); - if (!result.success) throw new InputValidationError(z.prettifyError(result.error)); - return result.data; -} diff --git a/src/handlers/project/create/create.screen.test.tsx b/src/handlers/project/create/create.screen.test.tsx index db7f3a26e..c2267c34e 100644 --- a/src/handlers/project/create/create.screen.test.tsx +++ b/src/handlers/project/create/create.screen.test.tsx @@ -267,7 +267,7 @@ describe("project create wizard", () => { r.unmount(); }); - test("template flow: strands with the default memory choice", async () => { + test("template flow: strands goes straight to review (no memory question)", async () => { const directory = await inTempDirectory(); const core = new TestCoreClient(); const inputs = spyOnCreate(core); @@ -282,32 +282,17 @@ describe("project create wizard", () => { await waitForText(r.lastFrame, "● scaffolded agent code"); await r.press("return"); - // Template step: the supported templates are offered. + // Template step: the supported templates are offered, including the + // -container variants and the empty project. await waitForText(r.lastFrame, "choose a template"); - expect(r.lastFrame()).toContain("mcp-python-fastmcp"); - expect(r.lastFrame()).toContain("a2a-python-strands"); expect(r.lastFrame()).toContain("● agent-python-strands (recommended)"); - const templateFrame = r.lastFrame() ?? ""; - expect(templateFrame.indexOf("agent-python-strands")).toBeLessThan( - templateFrame.indexOf("mcp-python-fastmcp"), - ); - await r.press("return"); - - // Memory step: asked only for strands; long and short-term preselected. - await waitForText(r.lastFrame, "choose a memory configuration"); - expect(r.lastFrame()).toContain("● long and short-term"); - const memoryFrame = r.lastFrame() ?? ""; - expect(memoryFrame.indexOf("long and short-term")).toBeLessThan(memoryFrame.indexOf("none")); + expect(r.lastFrame()).toContain("agent-python-strands-container"); await r.press("return"); + // No memory step: memory is no longer a choice, so review follows directly. await waitForText(r.lastFrame, "this project will be created"); - const reviewLines = (r.lastFrame() ?? "").split("\n"); - const reviewHeading = reviewLines.findIndex((line) => - line.includes("this project will be created"), - ); - expect(reviewLines[reviewHeading + 1] ?? "").toContain("─"); + expect(r.lastFrame()).not.toContain("choose a memory configuration"); expect(r.lastFrame()).toContain("agent-python-strands"); - expect(r.lastFrame()).toContain("long and short-term"); await r.press("return"); await waitForText(r.lastFrame, "✔ project created in ./StrandsApp", 5000); @@ -327,113 +312,73 @@ describe("project create wizard", () => { expect(spec.runtimes.map((runtime: { name: string }) => runtime.name)).toEqual([ "agent_python_strands", ]); + // The strands template ships with longAndShortTerm memory pre-configured. expect(spec.memories).toHaveLength(1); r.unmount(); }, 10000); - test("template flow: choosing no memory overrides the strands default", async () => { - await inTempDirectory(); + test("template flow: the minimal template scaffolds without memory", async () => { + const directory = await inTempDirectory(); const core = new TestCoreClient(); const inputs = spyOnCreate(core); const r = renderScreen("/agentcore/project/create", { core }); await waitForText(r.lastFrame, "name your project"); - await r.write("BareStrands"); + await r.write("HelloApp"); await r.press("return"); await waitForText(r.lastFrame, "what should the project be built around?"); await r.press("down"); await r.press("return"); await waitForText(r.lastFrame, "choose a template"); - await r.press("return"); // agent-python-strands is preselected - await waitForText(r.lastFrame, "choose a memory configuration"); - await r.press("down"); // none - await waitForText(r.lastFrame, "● none"); + await r.press("down"); // agent-python-strands-container + await r.press("down"); // agent-python-minimal + await waitForText(r.lastFrame, "● agent-python-minimal "); await r.press("return"); + + // Straight to review. await waitForText(r.lastFrame, "this project will be created"); await r.press("return"); - await waitForText(r.lastFrame, "✔ project created in ./BareStrands", 5000); + await waitForText(r.lastFrame, "✔ project created in ./HelloApp", 5000); expect(inputs[0]).toEqual({ - name: "BareStrands", + name: "HelloApp", skipInstall: false, skipGit: false, - scaffoldRuntimeInput: resolveRuntimeTemplateShortcut("agent-python-strands", { - memory: "none", - }), + scaffoldRuntimeInput: resolveRuntimeTemplateShortcut("agent-python-minimal"), }); + + const spec = await Bun.file(join(directory, "HelloApp", "agentcore", "agentcore.json")).json(); + expect(spec.memories ?? []).toHaveLength(0); r.unmount(); }, 10000); - test.each([ - ["agent-python-strands-container", 1], - ["a2a-python-strands", 4], - ] as const)( - "template flow: %s asks about memory", - async (template, downPresses) => { - await inTempDirectory(); - const core = new TestCoreClient(); - const inputs = spyOnCreate(core); - const r = renderScreen("/agentcore/project/create", { core }); - - await waitForText(r.lastFrame, "name your project"); - await r.write("StrandsVariant"); - await r.press("return"); - await waitForText(r.lastFrame, "what should the project be built around?"); - await r.press("down"); - await r.press("return"); - await waitForText(r.lastFrame, "choose a template"); - for (let i = 0; i < downPresses; i++) await r.press("down"); - await waitForText(r.lastFrame, `● ${template}`); - await r.press("return"); - await waitForText(r.lastFrame, "choose a memory configuration"); - await r.press("return"); - await waitForText(r.lastFrame, "this project will be created"); - await r.press("return"); - await waitForText(r.lastFrame, "project created in ./StrandsVariant", 5000); - - expect(inputs).toEqual([ - { - name: "StrandsVariant", - skipInstall: false, - skipGit: false, - scaffoldRuntimeInput: resolveRuntimeTemplateShortcut(template), - }, - ]); - r.unmount(); - }, - 10000, - ); - - test("template flow: hello-world skips the memory question", async () => { - await inTempDirectory(); + test("template flow: the empty template creates a project with no runtime", async () => { + const directory = await inTempDirectory(); const core = new TestCoreClient(); const inputs = spyOnCreate(core); const r = renderScreen("/agentcore/project/create", { core }); await waitForText(r.lastFrame, "name your project"); - await r.write("HelloApp"); + await r.write("EmptyApp"); await r.press("return"); await waitForText(r.lastFrame, "what should the project be built around?"); await r.press("down"); await r.press("return"); await waitForText(r.lastFrame, "choose a template"); - await r.press("down"); - await r.press("down"); // agent-python - await waitForText(r.lastFrame, "● agent-python "); + // empty is the last option in the list. + for (let i = 0; i < 10; i++) await r.press("down"); + await waitForText(r.lastFrame, "● empty"); await r.press("return"); - // Straight to review: hello-world does not support memory. await waitForText(r.lastFrame, "this project will be created"); - expect(r.lastFrame()).not.toContain("memory"); await r.press("return"); - await waitForText(r.lastFrame, "✔ project created in ./HelloApp", 5000); + await waitForText(r.lastFrame, "project created in ./EmptyApp", 5000); - expect(inputs[0]).toEqual({ - name: "HelloApp", - skipInstall: false, - skipGit: false, - scaffoldRuntimeInput: resolveRuntimeTemplateShortcut("agent-python"), - }); + expect(inputs[0]).toEqual({ name: "EmptyApp", skipInstall: false, skipGit: false }); + + const spec = await Bun.file(join(directory, "EmptyApp", "agentcore", "agentcore.json")).json(); + expect(spec.runtimes ?? []).toHaveLength(0); + expect(spec.harnesses ?? []).toHaveLength(0); r.unmount(); }, 10000); diff --git a/src/handlers/project/create/index.ts b/src/handlers/project/create/index.ts index f889a2f70..96b9e0862 100644 --- a/src/handlers/project/create/index.ts +++ b/src/handlers/project/create/index.ts @@ -3,70 +3,32 @@ import { createHandler, flag } from "../../../router"; import { SourceResolver, type AppIO } from "../../../io"; import { runWithProgress } from "../../../tui/progress"; import { - LANGUAGE_VERSION_DEFAULTS, - MEMORY_SHORTCUT_NAMES, - MEMORY_SHORTCUTS, - RUNTIME_TEMPLATE_SHORTCUT_NAMES, + EMPTY_TEMPLATE_NAME, + PROJECT_TEMPLATE_NAMES, + RUNTIME_TEMPLATE_SHORTCUTS, resolveRuntimeTemplateShortcut, } from "../shortcuts"; import { - ScaffoldRuntimeInputSchema, type CreateProjectInput, type ModelProvider, type ProjectManager, type ScaffoldHarnessInput, - type ScaffoldRuntimeInput, } from "../types"; import { ProjectNameSchema } from "../../../projectSchemas/project"; import { - CONTAINER_URI_PATTERN, HarnessModelProviderSchema, HarnessSpecSchema, type HarnessModelProvider, } from "../../../projectSchemas/harness"; import { InputValidationError } from "../../../errors"; -import { parseJsonFlag } from "../../utils"; import { DEFAULT_HARNESS_MODEL } from "../add/harness"; -import type { CoreBedrockAgentImporter } from "../../../core/project/bedrockAgentImport"; -import { importScaffoldRuntimeInput, resolveImportBedrockAgentInput } from "../importBedrockAgent"; -import type { ImportBedrockAgentInput } from "../add/runtime/types"; -import { JsonKey, RegionKey } from "../../keys"; +import { JsonKey } from "../../keys"; type CreateProjectHandlerConfig = { projectManager: ProjectManager; io: AppIO; - bedrockAgentImporter: CoreBedrockAgentImporter; }; -// Flags that select the runtime-scaffolding path. Any of these (or --template) -// present routes create away from the default harness path, mirroring the -// original CLI's agent-path dispatch. -const RUNTIME_PATH_FLAGS = [ - "build", - "language", - "framework", - "protocol", - "api-key", - "runtime-name", - "memory", - "type", - "agent-id", - "agent-alias-id", -] as const; - -// Flags that only make sense for the harness path. -const HARNESS_ONLY_FLAGS = [ - "model-id", - "api-key-arn", - "api-base", - "additional-params", - "max-iterations", - "max-tokens", - "timeout", - "truncation-strategy", - "container", -] as const; - const ModelProviderFlagSchema = z.enum([...HarnessModelProviderSchema.options, "anthropic"]); type ModelProviderFlag = z.infer; @@ -88,33 +50,12 @@ export const createCreateProjectHandler = (config: CreateProjectHandlerConfig) = flag("name", "name of the project to create", ProjectNameSchema.optional()), flag( "template", - "a preset of flags for scaffolding the Runtime; compatible flags override preset values", - z.enum(RUNTIME_TEMPLATE_SHORTCUT_NAMES).optional(), - ), - flag( - "build", - "build type for the scaffolded Runtime code", - z.enum(["CodeZip", "Container"]).optional(), - ), - flag( - "language", - "target language for the scaffolded Runtime code", - z.enum(["Python", "TypeScript"]).optional(), - ), - flag( - "framework", - "agent framework: strands or none for create; strands or langgraph for import", - z.enum(["strands", "langgraph", "none"]).optional(), - ), - flag( - "protocol", - "server protocol: HTTP, MCP, or A2A", - z.enum(["HTTP", "MCP", "A2A"]).optional(), + "the template to scaffold the Runtime from; some templates also accept --model-provider/--api-key", + z.enum(PROJECT_TEMPLATE_NAMES).optional(), ), flag( "model-provider", - "model provider: bedrock, open_ai, gemini, or lite_llm for harnesses; " + - "bedrock, anthropic, open_ai, or gemini for Runtime code", + "model provider for templates that support it: bedrock, anthropic, open_ai, gemini, or lite_llm", ModelProviderFlagSchema.optional(), ), flag( @@ -123,66 +64,6 @@ export const createCreateProjectHandler = (config: CreateProjectHandlerConfig) = z.string().optional(), { sensitive: true }, ), - flag( - "memory", - "memory option for the scaffolded Runtime", - z.enum(MEMORY_SHORTCUT_NAMES).optional(), - ), - flag("runtime-name", "name of the scaffolded Runtime", z.string().max(42).optional()), - flag( - "type", - "create scaffolds new agent code (the default); import translates a Bedrock Agent version", - z.enum(["create", "import"]).optional(), - ), - flag( - "agent-id", - "Bedrock Agent ID to import (requires --type import)", - z.string().optional(), - ), - flag( - "agent-alias-id", - "Bedrock Agent Alias ID selecting the version to import; must point at a prepared " + - "version, not DRAFT (requires --type import)", - z.string().optional(), - ), - flag("model-id", "model ID for the created harness", z.string().optional()), - flag( - "api-key-arn", - "API key credential ARN for the created harness's model provider", - z.string().optional(), - ), - flag( - "api-base", - "base URL for the harness model provider API endpoint (lite_llm)", - z.string().optional(), - ), - flag( - "additional-params", - "provider-specific harness model params as a JSON object (lite_llm)", - z.string().optional(), - ), - flag( - "harness-memory", - "disable memory for the created harness (this is the default)", - z.boolean().default(true), - ), - flag( - "max-iterations", - "max agent loop iterations per invocation (harness)", - z.number().optional(), - ), - flag("max-tokens", "max total output tokens per invocation (harness)", z.number().optional()), - flag("timeout", "max duration in seconds per invocation (harness)", z.number().optional()), - flag( - "truncation-strategy", - "context truncation strategy for the harness", - z.enum(["sliding_window", "summarization"]).optional(), - ), - flag( - "container", - "container image URI or Dockerfile path for the harness", - z.string().optional(), - ), flag( "skip-install", "skip installing dependencies (npm install, uv sync)", @@ -196,105 +77,47 @@ export const createCreateProjectHandler = (config: CreateProjectHandlerConfig) = throw new InputValidationError("required option '--name ' not specified"); } - const presentRuntimeFlags: string[] = RUNTIME_PATH_FLAGS.filter( - (f) => flags[f] !== undefined, - ); - const isTemplate = flags["template"] !== undefined; - if (isTemplate) presentRuntimeFlags.unshift("template"); + const template = flags["template"]; + const modelProviderFlag = flags["model-provider"]; + const apiKeyFlag = flags["api-key"]; - const presentHarnessFlags: string[] = HARNESS_ONLY_FLAGS.filter( - (f) => flags[f] !== undefined, - ); - if (flags["harness-memory"] === false) presentHarnessFlags.push("no-harness-memory"); - - // Mirrors the original CLI's dispatch: mixing the two paths is an error. - if (presentRuntimeFlags.length > 0 && presentHarnessFlags.length > 0) { - throw new InputValidationError( - `Cannot mix runtime scaffolding flags (${formatFlagList(presentRuntimeFlags)}) ` + - `with harness-only flags (${formatFlagList(presentHarnessFlags)}). ` + - `A project is created around either a harness (the default) or scaffolded runtime code.`, - ); - } - - const lockedFlag = (["language", "framework", "protocol"] as const).find( + const runtimeCodeFlags = (["model-provider", "api-key"] as const).filter( (flagName) => flags[flagName] !== undefined, ); - if (isTemplate && lockedFlag) { - throw new InputValidationError(`--${lockedFlag} cannot override a template`); - } - - const isImport = flags["type"] === "import"; - const scaffoldingChoiceFlags = - // --framework and --memory are import inputs, not scaffolding choices, so they are - // validated below instead of rejected here. - (["build", "language", "model-provider", "api-key"] as const).filter( - (f) => flags[f] !== undefined, - ); - if (isImport && (isTemplate || scaffoldingChoiceFlags.length > 0)) { - const offending = isTemplate ? "template" : scaffoldingChoiceFlags[0]; - throw new InputValidationError( - `--type import translates a Bedrock Agent into Python CodeZip runtime code; ` + - `--${offending} cannot be combined with it`, - ); - } - if (!isImport && (flags["agent-id"] !== undefined || flags["agent-alias-id"] !== undefined)) { - throw new InputValidationError("--agent-id and --agent-alias-id require --type import"); - } - if (isImport && flags["framework"] === "none") { - throw new InputValidationError("--type import supports --framework strands or langgraph"); - } - if (!isImport && flags["framework"] === "langgraph") { - throw new InputValidationError("--framework langgraph requires --type import"); - } - if (isImport && flags["protocol"] !== undefined && flags["protocol"] !== "HTTP") { - throw new InputValidationError("an imported Bedrock Agent only supports HTTP"); - } - - const isRuntimePath = presentRuntimeFlags.length > 0; - - let importBedrockAgent: ImportBedrockAgentInput | undefined; - const runtimeName = flags["runtime-name"] ?? name; - const importMemory = flags["memory"] ?? "none"; - if (isImport) { - importBedrockAgent = await resolveImportBedrockAgentInput({ - importer: config.bedrockAgentImporter, - runtimeName, - region: ctx.require(RegionKey), - agentId: flags["agent-id"], - agentAliasId: flags["agent-alias-id"], - framework: flags["framework"] === "langgraph" ? "langgraph" : "strands", - memory: importMemory, - }); - if (importBedrockAgent.notes.length > 0) { - config.io.stderr.write( - `Import generated ${importBedrockAgent.notes.length} manual follow-up ` + - `${importBedrockAgent.notes.length === 1 ? "item" : "items"} in ` + - `app/${runtimeName}/IMPORT_NOTES.md.\n`, + if (runtimeCodeFlags.length > 0) { + if (template === undefined || template === EMPTY_TEMPLATE_NAME) { + throw new InputValidationError( + `--${runtimeCodeFlags[0]} only applies to runtime templates`, + ); + } + if (!RUNTIME_TEMPLATE_SHORTCUTS[template].supportsModelProviderOverride) { + throw new InputValidationError( + `--${runtimeCodeFlags[0]} is not valid with the ${template} template`, ); } } - const createInput: CreateProjectInput = isRuntimePath - ? { - name, - skipInstall: flags["skip-install"], - skipGit: flags["skip-git"], - scaffoldRuntimeInput: isImport - ? importScaffoldRuntimeInput(runtimeName, MEMORY_SHORTCUTS[importMemory](runtimeName)) - : await resolveScaffoldRuntimeInput(config, { ...flags, name }), - importBedrockAgent, - } - : { - name, - skipInstall: flags["skip-install"], - skipGit: flags["skip-git"], - scaffoldHarnessInput: resolveScaffoldHarnessInput({ ...flags, name }), - }; - - if (!isRuntimePath && presentHarnessFlags.length === 0) { - config.io.stderr.write( - "Creating a harness project (pass --framework or --template to scaffold agent code instead).\n", - ); + const base = { + name, + skipInstall: flags["skip-install"], + skipGit: flags["skip-git"], + }; + + let createInput: CreateProjectInput; + if (template === undefined) { + createInput = { ...base, scaffoldHarnessInput: resolveScaffoldHarnessInput({ name }) }; + } else if (template === EMPTY_TEMPLATE_NAME) { + createInput = { ...base }; + } else { + const source = new SourceResolver({ stdin: config.io.stdin }); + const apiKey = await source.resolveSecret("api-key", apiKeyFlag); + createInput = { + ...base, + scaffoldRuntimeInput: resolveRuntimeTemplateShortcut(template, { + modelProvider: resolveRuntimeModelProvider(modelProviderFlag), + apiKey, + }), + }; } // Same driver as build and deploy: a live step list in a TTY, and the previous plain @@ -309,78 +132,20 @@ export const createCreateProjectHandler = (config: CreateProjectHandlerConfig) = }, }); -type RuntimePathFlagValues = { - name: string; - template?: (typeof RUNTIME_TEMPLATE_SHORTCUT_NAMES)[number]; - build?: "CodeZip" | "Container"; - language?: "Python" | "TypeScript"; - framework?: "strands" | "langgraph" | "none"; - protocol?: "HTTP" | "MCP" | "A2A"; - "model-provider"?: ModelProviderFlag; - "api-key"?: string; - memory?: (typeof MEMORY_SHORTCUT_NAMES)[number]; - "runtime-name"?: string; -}; - type HarnessPathFlagValues = { name: string; "model-provider"?: ModelProviderFlag; "model-id"?: string; "api-key-arn"?: string; "api-base"?: string; - "additional-params"?: string; - "max-iterations"?: number; - "max-tokens"?: number; - timeout?: number; - "truncation-strategy"?: "sliding_window" | "summarization"; - container?: string; }; -async function resolveScaffoldRuntimeInput( - config: CreateProjectHandlerConfig, - flags: RuntimePathFlagValues, -): Promise { - const modelProvider = resolveRuntimeModelProvider(flags["model-provider"]); - const source = new SourceResolver({ stdin: config.io.stdin }); - const apiKey = await source.resolveSecret("api-key", flags["api-key"]); - - const runtimeName = flags["runtime-name"] ?? flags["name"]; - const defaultMemory = flags["framework"] === "strands" ? "longAndShortTerm" : "none"; - - return flags["template"] !== undefined - ? resolveRuntimeTemplateShortcut(flags["template"], { - runtimeName: flags["runtime-name"], - build: flags["build"], - modelProvider, - apiKey, - memory: flags["memory"], - }) - : parseScaffoldRuntimeInput({ - runtimeName, - build: flags["build"], - language: flags["language"], - framework: flags["framework"] === "langgraph" ? undefined : flags["framework"], - protocol: flags["protocol"], - modelProvider, - apiKey, - memory: MEMORY_SHORTCUTS[flags["memory"] ?? defaultMemory](runtimeName), - runtimeVersion: - flags["build"] === "CodeZip" - ? LANGUAGE_VERSION_DEFAULTS[flags["language"] ?? "Python"] - : undefined, - }); -} - // The harness input validates against the same schema `project add harness` // uses, before any file is written; the manager then scaffolds it through the // same addResource path. Exported so the TUI create wizard builds its harness // input through the exact same translation as the flag-driven path. export function resolveScaffoldHarnessInput(flags: HarnessPathFlagValues): ScaffoldHarnessInput { const provider = resolveHarnessModelProvider(flags["model-provider"]); - const additionalParams = parseJsonFlag>( - "additional-params", - flags["additional-params"], - ); const input: ScaffoldHarnessInput = { // A project name always satisfies the harness name grammar (letters and @@ -392,17 +157,7 @@ export function resolveScaffoldHarnessInput(flags: HarnessPathFlagValues): Scaff modelId: flags["model-id"] ?? HARNESS_DEFAULT_MODEL_IDS[provider], apiKeyArn: flags["api-key-arn"], apiBase: flags["api-base"], - additionalParams, }, - maxIterations: flags["max-iterations"], - maxTokens: flags["max-tokens"], - timeoutSeconds: flags["timeout"], - truncation: flags["truncation-strategy"] - ? { strategy: flags["truncation-strategy"] } - : undefined, - // Harness memory is opt-in and disabled by default; --no-harness-memory - // documents the default explicitly. - ...parseContainerFlag(flags["container"]), }; const result = HarnessSpecSchema.safeParse(input); @@ -442,21 +197,3 @@ function resolveRuntimeModelProvider( ): ModelProvider | undefined { return providerFlag === undefined ? undefined : MODEL_PROVIDERS[providerFlag].runtime; } - -/** A --container value is either an ECR image URI or a local Dockerfile path. */ -function parseContainerFlag( - value: string | undefined, -): Pick { - if (value === undefined) return {}; - return CONTAINER_URI_PATTERN.test(value) ? { containerUri: value } : { dockerfile: value }; -} - -function formatFlagList(flagNames: string[]): string { - return flagNames.map((name) => `--${name}`).join(", "); -} - -function parseScaffoldRuntimeInput(input: Partial) { - const result = ScaffoldRuntimeInputSchema.safeParse(input); - if (!result.success) throw new InputValidationError(z.prettifyError(result.error)); - return result.data; -} diff --git a/src/handlers/project/create/screen.tsx b/src/handlers/project/create/screen.tsx index 3550eeecd..05987cdef 100644 --- a/src/handlers/project/create/screen.tsx +++ b/src/handlers/project/create/screen.tsx @@ -7,10 +7,9 @@ import type { HarnessModelProvider } from "../../../projectSchemas/harness"; import type { ScreenProps } from "../../types"; import type { CreateProjectInput } from "../types"; import { - RUNTIME_TEMPLATE_SHORTCUTS, + EMPTY_TEMPLATE_NAME, resolveRuntimeTemplateShortcut, - type MemoryShortcutName, - type RuntimeTemplateShortcutName, + type TemplateName, } from "../shortcuts"; import { HARNESS_DEFAULT_MODEL_IDS, resolveScaffoldHarnessInput } from "./index"; import { Layout } from "../../../components/Layout"; @@ -47,9 +46,7 @@ interface CreateProjectFormValues { name: string; kind: ProjectKind; model: ProjectModelValues; - // template + memory configure the agent path; memory applies to strands only. - template: RuntimeTemplateShortcutName; - memory: MemoryShortcutName; + template: TemplateName; } // defaultModelId is not declared here: the wizard and the flag path must offer @@ -99,7 +96,6 @@ function emptyCreateProjectForm(): CreateProjectFormValues { kind: "harness", model: emptyProjectModel(), template: "agent-python-strands", - memory: "longAndShortTerm", }; } @@ -117,7 +113,7 @@ const PROJECT_KIND_OPTIONS: { kind: ProjectKind; label: string; description: str ]; const TEMPLATE_OPTIONS: { - template: RuntimeTemplateShortcutName; + template: TemplateName; label: string; description: string; }[] = [ @@ -129,13 +125,18 @@ const TEMPLATE_OPTIONS: { { template: "agent-python-strands-container", label: "agent-python-strands-container", - description: "Strands agent on Bedrock with memory (Container build)", + description: "Strands agent on Bedrock with memory (container build)", }, { - template: "agent-python", - label: "agent-python", + template: "agent-python-minimal", + label: "agent-python-minimal", description: "minimal Python agent on Bedrock, no framework (CodeZip build)", }, + { + template: "agent-typescript-strands", + label: "agent-typescript-strands", + description: "Strands agent on Bedrock with memory, in TypeScript (CodeZip build)", + }, { template: "mcp-python-fastmcp", label: "mcp-python-fastmcp", @@ -151,22 +152,10 @@ const TEMPLATE_OPTIONS: { label: "agui-python-strands", description: "Strands agent speaking the AG-UI protocol on Bedrock (CodeZip build)", }, -]; - -const asksMemory = (template: RuntimeTemplateShortcutName) => - RUNTIME_TEMPLATE_SHORTCUTS[template].framework === "strands"; - -const MEMORY_OPTIONS: { memory: MemoryShortcutName; label: string; description: string }[] = [ - { - memory: "longAndShortTerm", - label: "long and short-term", - description: "session events plus long-term memory strategies (recommended)", - }, - { memory: "none", label: "none", description: "no memory resources" }, { - memory: "shortTerm", - label: "short-term", - description: "raw session events, 30-day expiry", + template: EMPTY_TEMPLATE_NAME, + label: "empty", + description: "an empty project with no runtime or harness", }, ]; @@ -196,14 +185,14 @@ export function buildCreateInput(values: CreateProjectFormValues): CreateProject }), }; } + if (values.template === EMPTY_TEMPLATE_NAME) { + return { name: values.name, skipInstall: false, skipGit: false }; + } return { name: values.name, skipInstall: false, skipGit: false, - scaffoldRuntimeInput: resolveRuntimeTemplateShortcut( - values.template, - asksMemory(values.template) ? { memory: values.memory } : undefined, - ), + scaffoldRuntimeInput: resolveRuntimeTemplateShortcut(values.template), }; } @@ -223,14 +212,8 @@ function summaryOf(values: CreateProjectFormValues): Record { directory: `./${values.name}`, }; } - const withTemplate = { ...base, type: "agent code", template: values.template }; - return asksMemory(values.template) - ? { - ...withTemplate, - memory: MEMORY_OPTIONS.find((option) => option.memory === values.memory)!.label, - directory: `./${values.name}`, - } - : { ...withTemplate, directory: `./${values.name}` }; + const type = values.template === EMPTY_TEMPLATE_NAME ? "empty project" : "agent code"; + return { ...base, type, template: values.template, directory: `./${values.name}` }; } function providerLabel(provider: HarnessModelProvider): string { @@ -246,7 +229,7 @@ type WizardPhase = { kind: "form" } | { kind: "running" } | { kind: "success" } | { kind: "error"; error: Error }; // ProjectCreateScreen is the interactive flow behind a bare `agentcore project -// create`: name → type → (model | template [→ memory]) → review, then the +// create`: name → type → (model | template) → review, then the // creation itself, streaming the ProjectManager's progress events. It drives // core.projectManager.create with the same input the flag-driven handler // builds, so both entry points scaffold identical projects — in the current @@ -261,22 +244,19 @@ export function ProjectCreateScreen({ core }: ScreenProps) { const [tasks, setTasks] = useState([]); // The step list is dynamic: the branch chosen on the type step decides - // whether model or template (and, for strands, memory) questions follow. + // whether the model or the template question follows. const steps: Step[] = useMemo(() => { const branch: Step[] = values.kind === "harness" ? [{ key: "model", title: "model" }] - : [ - { key: "template", title: "template" }, - ...(asksMemory(values.template) ? [{ key: "memory", title: "memory" }] : []), - ]; + : [{ key: "template", title: "template" }]; return [ { key: "name", title: "name" }, { key: "type", title: "type" }, ...branch, { key: "review", title: "review" }, ]; - }, [values.kind, values.template]); + }, [values.kind]); const stepKey = steps[stepIndex]!.key; const patch = (update: Partial) => @@ -366,7 +346,6 @@ function hintsFor(stepKey: string, phase: WizardPhase): { key: string; label: st return [{ key: "↑↓", label: "navigate" }, { key: "enter", label: "continue" }, ...base]; case "type": case "template": - case "memory": return [{ key: "↑↓", label: "choose" }, { key: "enter", label: "continue" }, ...base]; case "review": return [{ key: "enter", label: "create" }, ...base]; @@ -430,18 +409,6 @@ function WizardStep({ stepKey, values, patch, onNext, onBack, onSubmit }: Wizard onBack={onBack} /> ); - case "memory": - return ( - option.memory === values.memory)} - onSelect={(index) => patch({ memory: MEMORY_OPTIONS[index]!.memory })} - onNext={onNext} - onBack={onBack} - /> - ); case "review": return ; default: diff --git a/src/handlers/project/export/harness.test.ts b/src/handlers/project/export/harness.test.ts index e2dcebe06..43da9635f 100644 --- a/src/handlers/project/export/harness.test.ts +++ b/src/handlers/project/export/harness.test.ts @@ -65,7 +65,7 @@ async function inProjectWithHarness( "--name", "orders", "--template", - "agent-python", + "agent-python-minimal", "--skip-install", "--skip-git", ]); @@ -155,8 +155,8 @@ describe("project export harness handler", () => { // The scaffolded template runtime already owns its name. await expect( - subject.run(["--name", "exportme", "--target-agent-name", "agent_python"]), - ).rejects.toThrow(/runtime with name 'agent_python' already exists/); + subject.run(["--name", "exportme", "--target-agent-name", "agent_python_minimal"]), + ).rejects.toThrow(/runtime with name 'agent_python_minimal' already exists/); // A harness name is just as taken. await expect( subject.run(["--name", "exportme", "--target-agent-name", "exportme"]), diff --git a/src/handlers/project/importBedrockAgent.test.ts b/src/handlers/project/importBedrockAgent.test.ts index 2d32a49cf..3bd5c8fb3 100644 --- a/src/handlers/project/importBedrockAgent.test.ts +++ b/src/handlers/project/importBedrockAgent.test.ts @@ -5,7 +5,6 @@ import type { CoreBedrockAgentImporter, } from "../../core/project/bedrockAgentImport"; import { InputValidationError } from "../../errors"; -import { MEMORY_SHORTCUTS } from "./shortcuts"; import { importScaffoldRuntimeInput, resolveImportBedrockAgentInput } from "./importBedrockAgent"; const plan: BedrockAgentImportPlan = { @@ -82,16 +81,14 @@ describe("resolveImportBedrockAgentInput", () => { }); describe("importScaffoldRuntimeInput", () => { - test("uses the fixed Python CodeZip runtime shape and selected memory", () => { - const memory = MEMORY_SHORTCUTS.longAndShortTerm("support"); - - expect(importScaffoldRuntimeInput("support", memory)).toEqual({ + test("uses the fixed Python CodeZip runtime shape with no memory", () => { + expect(importScaffoldRuntimeInput("support")).toEqual({ runtimeName: "support", build: "CodeZip", language: "Python", framework: "none", modelProvider: "Bedrock", - memory, + memory: undefined, runtimeVersion: "PYTHON_3_14", }); }); diff --git a/src/handlers/project/index.ts b/src/handlers/project/index.ts index 0d4b22794..1791cc291 100644 --- a/src/handlers/project/index.ts +++ b/src/handlers/project/index.ts @@ -49,7 +49,6 @@ export function createProjectHandler({ core, io }: ProjectHandlerConfig): Router const createProject = createCreateProjectHandler({ projectManager, io, - bedrockAgentImporter: core.bedrockAgentImporter, }); const createProjectWithWizard = withTuiOnEmptyFlagsAndArgs(core, io)(createProject); const isInteractive = () => io.stdin.isTTY === true && io.stdout.isTTY === true; diff --git a/src/handlers/project/project.test.ts b/src/handlers/project/project.test.ts index f053cbebe..3d071a0d2 100644 --- a/src/handlers/project/project.test.ts +++ b/src/handlers/project/project.test.ts @@ -11,7 +11,6 @@ import { testIO, } from "../../testing"; import { InputValidationError } from "../../errors"; -import type { BedrockAgentImportPlan } from "../../core/project/bedrockAgentImport"; async function run(args: string[], opts?: { core?: TestCoreClient; stdin?: string }) { const io = testIO({ stdin: opts?.stdin }); @@ -38,21 +37,6 @@ test("project dev requires an AgentCore project", async () => { const originalCwd = process.cwd(); const tempDirectories: string[] = []; -function translatedImportPlan(): BedrockAgentImportPlan { - return { - framework: "strands", - sourceAgentId: "A1B2C3D4E5", - sourceAgentAliasId: "TSTALIASID", - sourceAgentVersion: "7", - files: { - "main.py": "from strands import Agent\n# translated", - "pyproject.toml": '[project]\nname = "my-import"\n', - "IMPORT_NOTES.md": "# Bedrock Agent Import Notes\n", - }, - notes: [], - }; -} - async function inTempDirectory(): Promise { const directory = await mkdtemp(join(tmpdir(), "agentcore-project-")); tempDirectories.push(directory); @@ -81,7 +65,7 @@ async function inProject(name = "TestProject"): Promise { describe("project create", () => { test("scaffolds a harness project by default, named for the project", async () => { const directory = await inTempDirectory(); - const { io } = await run(["create", "--name", "MyAgent"]); + await run(["create", "--name", "MyAgent"]); const projectRoot = join(directory, "MyAgent"); const spec = await Bun.file(join(projectRoot, "agentcore", "agentcore.json")).json(); @@ -97,269 +81,102 @@ describe("project create", () => { expect(await Bun.file(join(projectRoot, "app", "MyAgent", "system-prompt.md")).exists()).toBe( true, ); - expect(io.stderr()).toContain("Creating a harness project"); - }); - - test("rejects the removed --defaults flag", async () => { - await inTempDirectory(); - await expect(run(["create", "--name", "MyAgent", "--defaults"])).rejects.toThrow( - /unknown option '--defaults'/, - ); }); - test("harness-only flags flow into the harness spec", async () => { + test("a harness create installs CDK dependencies and git only (no uv sync)", async () => { const directory = await inTempDirectory(); - await run([ - "create", - "--name", - "MyAgent", - "--model-id", - "us.amazon.nova-lite-v1:0", - "--api-key-arn", - "arn:aws:bedrock-agentcore:us-east-1:111122223333:token-vault/default/apikeycredentialprovider/k", - "--max-iterations", - "5", - "--max-tokens", - "2048", - "--timeout", - "60", - "--truncation-strategy", - "sliding_window", - "--no-harness-memory", - ]); + const { core } = await run(["create", "--name", "MyAgent"]); - const harness = await Bun.file( - join(directory, "MyAgent", "app", "MyAgent", "harness.json"), - ).json(); - expect(harness).toMatchObject({ - model: { - provider: "bedrock", - modelId: "us.amazon.nova-lite-v1:0", - apiKeyArn: - "arn:aws:bedrock-agentcore:us-east-1:111122223333:token-vault/default/apikeycredentialprovider/k", + const projectRoot = join(directory, "MyAgent"); + expect(core.projectCommands).toEqual([ + { + command: ["npm", "install", "--loglevel=http"], + cwd: join(projectRoot, "agentcore", "cdk"), }, - maxIterations: 5, - maxTokens: 2048, - timeoutSeconds: 60, - truncation: { strategy: "sliding_window" }, - }); - expect(harness.memory).toBeUndefined(); - }); - - test.each([ - ["bedrock", "global.anthropic.claude-sonnet-4-6", undefined], - [ - "open_ai", - "gpt-5", - "arn:aws:bedrock-agentcore:us-east-1:111122223333:token-vault/default/apikeycredentialprovider/openai", - ], - [ - "gemini", - "gemini-2.5-flash", - "arn:aws:bedrock-agentcore:us-east-1:111122223333:token-vault/default/apikeycredentialprovider/gemini", - ], - ])( - "--model-provider %s selects the harness path with its provider default", - async (provider, modelId, apiKeyArn) => { - const directory = await inTempDirectory(); - const args = ["create", "--name", "MyAgent", "--model-provider", provider]; - if (apiKeyArn) args.push("--api-key-arn", apiKeyArn); - - await run(args); - - const projectRoot = join(directory, "MyAgent"); - const spec = await Bun.file(join(projectRoot, "agentcore", "agentcore.json")).json(); - const harness = await Bun.file(join(projectRoot, "app", "MyAgent", "harness.json")).json(); - expect(spec.runtimes).toEqual([]); - expect(harness.model).toEqual({ - provider, - modelId, - ...(apiKeyArn && { apiKeyArn }), - }); - }, - ); - - test("rejects a runtime-only provider on the harness path", async () => { - await inTempDirectory(); - await expect( - run(["create", "--name", "MyAgent", "--model-provider", "anthropic"]), - ).rejects.toThrow(/'anthropic' model provider is not supported for harness projects/); - }); - - test("supports LiteLLM model configuration on the harness path", async () => { - const directory = await inTempDirectory(); - await run([ - "create", - "--name", - "MyAgent", - "--model-provider", - "lite_llm", - "--api-base", - "https://litellm.example.com/v1", - "--additional-params", - '{"max_retries":2}', + { command: ["git", "init"], cwd: projectRoot }, ]); - - const harness = await Bun.file( - join(directory, "MyAgent", "app", "MyAgent", "harness.json"), - ).json(); - expect(harness.model).toEqual({ - provider: "lite_llm", - modelId: "bedrock/global.anthropic.claude-sonnet-4-6", - apiBase: "https://litellm.example.com/v1", - additionalParams: { max_retries: 2 }, - }); }); - test("--container with an image URI records containerUri on the harness", async () => { + test("the empty template scaffolds a project with no runtime and no harness", async () => { const directory = await inTempDirectory(); await run([ "create", "--name", "MyAgent", - "--container", - "111122223333.dkr.ecr.us-east-1.amazonaws.com/agents:latest", + "--template", + "empty", + "--skip-install", + "--skip-git", ]); - const harness = await Bun.file( - join(directory, "MyAgent", "app", "MyAgent", "harness.json"), - ).json(); - expect(harness.containerUri).toBe("111122223333.dkr.ecr.us-east-1.amazonaws.com/agents:latest"); - expect(harness.dockerfile).toBeUndefined(); - }); - - test("--container with a Dockerfile path vendors the Dockerfile into the harness", async () => { - const directory = await inTempDirectory(); - await Bun.write(join(directory, "MyDockerfile"), "FROM public.ecr.aws/docker/library/python"); - await run(["create", "--name", "MyAgent", "--container", "MyDockerfile"]); - - const harnessRoot = join(directory, "MyAgent", "app", "MyAgent"); - const harness = await Bun.file(join(harnessRoot, "harness.json")).json(); - expect(harness.dockerfile).toBe("Dockerfile"); - expect(await Bun.file(join(harnessRoot, "Dockerfile")).text()).toContain("FROM "); + const projectRoot = join(directory, "MyAgent"); + const spec = await Bun.file(join(projectRoot, "agentcore", "agentcore.json")).json(); + expect(spec.runtimes ?? []).toEqual([]); + expect(spec.harnesses ?? []).toEqual([]); + expect(existsSync(join(projectRoot, "app"))).toBe(true); }); - test("rejects mixing runtime scaffolding flags with harness-only flags", async () => { + test("rejects --model-provider with the empty template", async () => { await inTempDirectory(); await expect( - run(["create", "--name", "MyAgent", "--framework", "strands", "--model-id", "x"]), - ).rejects.toThrow(/Cannot mix runtime scaffolding flags \(--framework\)/); - await expect( - run(["create", "--name", "MyAgent", "--template", "agent-python", "--timeout", "9"]), - ).rejects.toThrow(/harness-only flags \(--timeout\)/); - await expect( - run(["create", "--name", "MyAgent", "--template", "agent-python", "--no-harness-memory"]), - ).rejects.toThrow(/harness-only flags \(--no-harness-memory\)/); + run(["create", "--name", "MyAgent", "--template", "empty", "--model-provider", "anthropic"]), + ).rejects.toThrow(/--model-provider only applies to runtime templates/); }); - test("a harness create installs CDK dependencies and git only (no uv sync)", async () => { - const directory = await inTempDirectory(); - const { core } = await run(["create", "--name", "MyAgent"]); - - const projectRoot = join(directory, "MyAgent"); - expect(core.projectCommands).toEqual([ - { - command: ["npm", "install", "--loglevel=http"], - cwd: join(projectRoot, "agentcore", "cdk"), - }, - { command: ["git", "init"], cwd: projectRoot }, - ]); + test("rejects --model-provider without a template", async () => { + await inTempDirectory(); + await expect( + run(["create", "--name", "MyAgent", "--model-provider", "anthropic"]), + ).rejects.toThrow(/--model-provider only applies to runtime templates/); }); - test("--type import scaffolds a translated Bedrock Agent project", async () => { + test("rejects --api-key with a template that does not support it", async () => { const directory = await inTempDirectory(); - const core = new TestCoreClient(); - core.bedrockAgentImportPlans["A1B2C3D4E5/TSTALIASID"] = translatedImportPlan(); - - await run( - [ - "create", - "--name", - "MyImport", - "--type", - "import", - "--agent-id", - "A1B2C3D4E5", - "--agent-alias-id", - "TSTALIASID", - "--region", - "us-east-1", - ], - { core }, - ); - - const projectRoot = join(directory, "MyImport"); - const spec = await Bun.file(join(projectRoot, "agentcore", "agentcore.json")).json(); - expect(spec.harnesses).toBeUndefined(); - expect(spec.runtimes[0]).toMatchObject({ - name: "MyImport", - build: "CodeZip", - runtimeVersion: "PYTHON_3_14", - }); - - const main = await Bun.file(join(projectRoot, "app", "MyImport", "main.py")).text(); - expect(main).toContain("# translated"); - expect(main).not.toContain("client.invoke_agent"); - expect(core.importedBedrockAgents[0]).toMatchObject({ - runtimeName: "MyImport", - framework: "strands", - memory: "none", - }); + await expect( + run( + [ + "create", + "--name", + "MyProject", + "--template", + "agent-python-minimal", + "--api-key", + "-", + "--skip-install", + "--skip-git", + ], + { stdin: "secret-key" }, + ), + ).rejects.toThrow(/--api-key is not valid with the agent-python-minimal template/); + expect(existsSync(join(directory, "MyProject"))).toBe(false); }); - test("--type import conflicts with harness-only and incompatible scaffolding flags", async () => { + test("rejects --model-provider with a template that does not support it", async () => { await inTempDirectory(); - await expect( - run(["create", "--name", "MyImport", "--type", "import", "--model-id", "x"]), - ).rejects.toThrow(/Cannot mix runtime scaffolding flags \(--type\)/); await expect( run([ "create", "--name", - "MyImport", - "--type", - "import", - "--agent-id", - "A", - "--agent-alias-id", - "B", - "--build", - "Container", + "MyProject", + "--template", + "a2a-python-strands", + "--model-provider", + "anthropic", + "--skip-install", + "--skip-git", ]), - ).rejects.toThrow(/--build cannot be combined/); - await expect(run(["create", "--name", "MyImport", "--agent-id", "A"])).rejects.toThrow( - /--agent-id and --agent-alias-id require --type import/, - ); - }); - - test("rejects invalid harness flag combinations before scaffolding anything", async () => { - const directory = await inTempDirectory(); - // apiBase is a lite_llm-only model setting; the bedrock harness path - // surfaces the schema's guidance without writing a partial project. - await expect( - run(["create", "--name", "MyAgent", "--api-base", "https://example.com"]), - ).rejects.toThrow(/lite_llm/); - expect(await Bun.file(join(directory, "MyAgent")).exists()).toBe(false); - - await expect( - run(["create", "--name", "MyAgent", "--additional-params", "{not-json"]), - ).rejects.toThrow(/JSON/i); - expect(await Bun.file(join(directory, "MyAgent")).exists()).toBe(false); - }); - - test("rejects an invalid --project-name", async () => { - await inTempDirectory(); - await expect(run(["create", "--name", "1-bad"])).rejects.toThrow(); - }); - - test("rejects a reserved --project-name", async () => { - await inTempDirectory(); - await expect(run(["create", "--name", "test"])).rejects.toThrow(/conflicts with/); + ).rejects.toThrow(/--model-provider is not valid with the a2a-python-strands template/); }); test("runs the post-scaffold steps and reports progress on stderr", async () => { const directory = await inTempDirectory(); - const { io, core } = await run(["create", "--name", "MyAgent", "--template", "agent-python"]); + const { io, core } = await run([ + "create", + "--name", + "MyAgent", + "--template", + "agent-python-minimal", + ]); const projectRoot = join(directory, "MyAgent"); expect(core.projectCommands).toEqual([ @@ -367,7 +184,7 @@ describe("project create", () => { command: ["npm", "install", "--loglevel=http"], cwd: join(projectRoot, "agentcore", "cdk"), }, - { command: ["uv", "sync"], cwd: join(projectRoot, "app", "agent_python") }, + { command: ["uv", "sync"], cwd: join(projectRoot, "app", "agent_python_minimal") }, { command: ["git", "init"], cwd: projectRoot }, ]); expect(io.stderr()).toContain("Creating project tree"); @@ -384,17 +201,7 @@ describe("project create", () => { expect(core.projectCommands).toEqual([]); }); - test.each([ - ["language", "Python"], - ["framework", "none"], - ])("rejects --%s as a template override", async (flagName, value) => { - await inTempDirectory(); - await expect( - run(["create", "--name", "MyAgent", "--template", "agent-python", `--${flagName}`, value]), - ).rejects.toThrow(`--${flagName} cannot override a template`); - }); - - test("applies compatible overrides to a template", async () => { + test("scaffolds the strands template with longAndShortTerm memory pre-configured", async () => { const directory = await inTempDirectory(); await run([ "create", @@ -402,14 +209,6 @@ describe("project create", () => { "MyProject", "--template", "agent-python-strands", - "--runtime-name", - "custom_agent", - "--build", - "CodeZip", - "--model-provider", - "bedrock", - "--memory", - "none", "--skip-install", "--skip-git", ]); @@ -417,12 +216,22 @@ describe("project create", () => { const projectRoot = join(directory, "MyProject"); const spec = await Bun.file(join(projectRoot, "agentcore", "agentcore.json")).json(); expect(spec.runtimes[0]).toMatchObject({ - name: "custom_agent", + name: "agent_python_strands", build: "CodeZip", - codeLocation: "app/custom_agent", + codeLocation: "app/agent_python_strands", runtimeVersion: "PYTHON_3_14", }); - expect(await Bun.file(join(projectRoot, "app", "custom_agent", "main.py")).exists()).toBe(true); + const memory = (spec.memories ?? [])[0]; + expect(memory).toMatchObject({ name: "agent_python_strandsMemory", eventExpiryDuration: 30 }); + expect(memory.strategies.map(({ type }: { type: string }) => type)).toEqual([ + "SEMANTIC", + "USER_PREFERENCE", + "SUMMARIZATION", + "EPISODIC", + ]); + expect( + await Bun.file(join(projectRoot, "app", "agent_python_strands", "main.py")).exists(), + ).toBe(true); }); test("scaffolds a keyless LiteLLM runtime with no credential", async () => { @@ -479,23 +288,28 @@ describe("project create", () => { expect(envLocal).toContain("test-api-key"); }); - test.each([ - ["--build Container override", ["--template", "agent-python-strands", "--build", "Container"]], - ["container template", ["--template", "agent-python-strands-container"]], - ])("scaffolds a Container agent from the strands template (%s)", async (_label, flags) => { + test("scaffolds a Container agent from the strands -container template", async () => { const directory = await inTempDirectory(); - await run(["create", "--name", "MyProject", ...flags, "--skip-install", "--skip-git"]); + await run([ + "create", + "--name", + "MyProject", + "--template", + "agent-python-strands-container", + "--skip-install", + "--skip-git", + ]); const projectRoot = join(directory, "MyProject"); const spec = await Bun.file(join(projectRoot, "agentcore", "agentcore.json")).json(); expect(spec.runtimes[0]).toMatchObject({ - name: "agent_python_strands", + name: "agent_python_strands_container", build: "Container", - codeLocation: "app/agent_python_strands", + codeLocation: "app/agent_python_strands_container", dockerfile: "Dockerfile", }); expect(spec.runtimes[0].runtimeVersion).toBeUndefined(); - const runtimeRoot = join(projectRoot, "app", "agent_python_strands"); + const runtimeRoot = join(projectRoot, "app", "agent_python_strands_container"); expect(await Bun.file(join(runtimeRoot, "Dockerfile")).exists()).toBe(true); expect(await Bun.file(join(runtimeRoot, ".dockerignore")).exists()).toBe(true); }); @@ -524,16 +338,14 @@ describe("project create", () => { "--name", "MyProject", "--template", - "agent-python-strands", - "--build", - "Container", + "agent-python-strands-container", "--skip-install", "--skip-git", ]); expect(core.projectCommands).toContainEqual({ command: ["uv", "lock"], - cwd: join(directory, "MyProject", "app", "agent_python_strands"), + cwd: join(directory, "MyProject", "app", "agent_python_strands_container"), }); }); @@ -563,97 +375,17 @@ describe("project create", () => { expect(mainPy).toContain("FastMCP"); expect(mainPy).toContain('mcp.run(transport="streamable-http")'); expect(await Bun.file(join(runtimeRoot, "Dockerfile")).exists()).toBe(false); + expect(spec.memories ?? []).toEqual([]); }); - test("scaffolds a Container MCP server from the mcp-python-fastmcp template", async () => { - const directory = await inTempDirectory(); - await run([ - "create", - "--name", - "MyProject", - "--template", - "mcp-python-fastmcp", - "--build", - "Container", - "--skip-install", - "--skip-git", - ]); - - const projectRoot = join(directory, "MyProject"); - const spec = await Bun.file(join(projectRoot, "agentcore", "agentcore.json")).json(); - expect(spec.runtimes[0]).toMatchObject({ - name: "mcp_python_fastmcp", - build: "Container", - protocol: "MCP", - dockerfile: "Dockerfile", - }); - expect(spec.runtimes[0].runtimeVersion).toBeUndefined(); - expect( - await Bun.file(join(projectRoot, "app", "mcp_python_fastmcp", "Dockerfile")).exists(), - ).toBe(true); - }); - - test.each([ - ["default", [], ["SEMANTIC", "USER_PREFERENCE", "SUMMARIZATION", "EPISODIC"]], - ["none", ["--memory", "none"], []], - ["short", ["--memory", "shortTerm"], []], - [ - "shortAndLongTerm", - ["--memory", "longAndShortTerm"], - ["SEMANTIC", "USER_PREFERENCE", "SUMMARIZATION", "EPISODIC"], - ], - ])("custom strands %s memory", async (_label, memoryFlags, expectedStrategies) => { - const directory = await inTempDirectory(); - await run([ - "create", - "--name", - "MyAgent", - "--build", - "CodeZip", - "--language", - "Python", - "--framework", - "strands", - "--model-provider", - "bedrock", - ...memoryFlags, - "--skip-install", - "--skip-git", - ]); - - const projectRoot = join(directory, "MyAgent"); - const spec = await Bun.file(join(projectRoot, "agentcore", "agentcore.json")).json(); - const memories = spec.memories ?? []; - const memory = memories[0]; - - if (memoryFlags.length > 1 && memoryFlags[1] === "none") { - expect(memories).toEqual([]); - return; - } - - expect(memory).toMatchObject({ - name: "MyAgentMemory", - eventExpiryDuration: 30, - }); - expect(memory.strategies.map(({ type }: { type: string }) => type)).toEqual(expectedStrategies); - }); - - test("scaffolds from explicit custom flags", async () => { + test("scaffolds the minimal Python template", async () => { const directory = await inTempDirectory(); await run([ "create", "--name", "MyAgent", - "--build", - "CodeZip", - "--language", - "Python", - "--framework", - "none", - "--model-provider", - "bedrock", - "--memory", - "none", + "--template", + "agent-python-minimal", "--skip-install", "--skip-git", ]); @@ -662,31 +394,24 @@ describe("project create", () => { const spec = await Bun.file(join(projectRoot, "agentcore", "agentcore.json")).json(); expect(spec.runtimes).toEqual([ { - name: "MyAgent", + name: "agent_python_minimal", build: "CodeZip", entrypoint: "main.py", - codeLocation: "app/MyAgent", + codeLocation: "app/agent_python_minimal", runtimeVersion: "PYTHON_3_14", }, ]); + expect(spec.memories ?? []).toEqual([]); }); - test("scaffolds a TypeScript strands runtime from custom flags", async () => { + test("scaffolds a TypeScript strands runtime with memory pre-configured", async () => { const directory = await inTempDirectory(); await run([ "create", "--name", "MyAgent", - "--build", - "CodeZip", - "--language", - "TypeScript", - "--framework", - "strands", - "--model-provider", - "bedrock", - "--memory", - "none", + "--template", + "agent-typescript-strands", "--skip-install", "--skip-git", ]); @@ -695,108 +420,28 @@ describe("project create", () => { const spec = await Bun.file(join(projectRoot, "agentcore", "agentcore.json")).json(); // NODE_22 runtimes deploy a compiled main.js, so the spec entrypoint is main.js // even though the scaffolded source is main.ts. - expect(spec.runtimes).toEqual([ - { - name: "MyAgent", - build: "CodeZip", - entrypoint: "main.js", - codeLocation: "app/MyAgent", - runtimeVersion: "NODE_22", - protocol: "HTTP", - }, - ]); - expect(await Bun.file(join(projectRoot, "app", "MyAgent", "main.ts")).exists()).toBe(true); + expect(spec.runtimes[0]).toMatchObject({ + name: "agent_typescript_strands", + build: "CodeZip", + entrypoint: "main.js", + codeLocation: "app/agent_typescript_strands", + runtimeVersion: "NODE_22", + protocol: "HTTP", + }); + expect(spec.memories ?? []).toHaveLength(1); + expect( + await Bun.file(join(projectRoot, "app", "agent_typescript_strands", "main.ts")).exists(), + ).toBe(true); }); - test.each(["shortTerm", "longAndShortTerm"] as const)( - "rejects --memory %s with --framework none", - async (memoryShortcut) => { - await inTempDirectory(); - await expect( - run([ - "create", - "--name", - "MyAgent", - "--build", - "CodeZip", - "--language", - "Python", - "--framework", - "none", - "--model-provider", - "bedrock", - "--memory", - memoryShortcut, - "--skip-install", - "--skip-git", - ]), - ).rejects.toBeInstanceOf(InputValidationError); - }, - ); - - test.each([ - ["path traversal", "../MyAgent", /Must begin with a letter/], - ["starts with a digit", "1Agent", /Must begin with a letter/], - ["contains a hyphen", "my-agent", /Must begin with a letter/], - ["contains a space", "my agent", /Must begin with a letter/], - ["exceeds 42 chars", "a".repeat(43), /<=42 characters/], - ])( - "rejects an invalid --runtime-name before scaffolding (%s)", - async (_label, runtimeName, expectedError) => { - const directory = await inTempDirectory(); - await expect( - run([ - "create", - "--name", - "MyProject", - "--runtime-name", - runtimeName, - "--build", - "CodeZip", - "--language", - "Python", - "--framework", - "none", - "--model-provider", - "bedrock", - "--memory", - "none", - "--skip-install", - "--skip-git", - ]), - ).rejects.toThrow(expectedError); - - expect(existsSync(join(directory, "MyProject"))).toBe(false); - }, - ); - - test("rejects an incompatible API-key template override before scaffolding", async () => { - const directory = await inTempDirectory(); - await expect( - run( - [ - "create", - "--name", - "MyProject", - "--template", - "agent-python", - "--api-key", - "-", - "--skip-install", - "--skip-git", - ], - { stdin: "secret-key" }, - ), - ).rejects.toThrow(/API keys are not compatible with Bedrock model providers/); - - expect(existsSync(join(directory, "MyProject"))).toBe(false); + test("rejects an invalid --name", async () => { + await inTempDirectory(); + await expect(run(["create", "--name", "1-bad"])).rejects.toThrow(); }); - test("rejects incomplete custom flags", async () => { + test("rejects a reserved --name", async () => { await inTempDirectory(); - await expect( - run(["create", "--name", "MyAgent", "--build", "CodeZip", "--language", "Python"]), - ).rejects.toThrow(); + await expect(run(["create", "--name", "test"])).rejects.toThrow(/conflicts with/); }); test("rejects an unknown --template value", async () => { diff --git a/src/handlers/project/remove/index.test.ts b/src/handlers/project/remove/index.test.ts index ef54af689..937109743 100644 --- a/src/handlers/project/remove/index.test.ts +++ b/src/handlers/project/remove/index.test.ts @@ -56,7 +56,7 @@ async function inProject(name = "TestProject"): Promise { "--name", name, "--template", - "agent-python", + "agent-python-minimal", "--skip-install", "--skip-git", ]); @@ -87,7 +87,7 @@ describe("project remove", () => { }, { label: "runtime", - commands: [["remove", "runtime", "--name", "agent_python"]], + commands: [["remove", "runtime", "--name", "agent_python_minimal"]], specKey: "runtimes", expectedRemaining: [], }, @@ -136,7 +136,7 @@ describe("project remove", () => { "--name", "quality", "--agent", - "agent_python", + "agent_python_minimal", "--evaluator", "Builtin.Correctness", "--sampling-rate", @@ -156,7 +156,7 @@ describe("project remove", () => { "--name", "failures", "--agent", - "agent_python", + "agent_python_minimal", "--insight", "Builtin.Insight.FailureAnalysis", "--sampling-rate", @@ -533,7 +533,7 @@ describe("project remove all", () => { expect(spec.managedBy).toBe(before.managedBy); // Removal stays spec-level: scaffolded code and the credential's env entry. - expect(existsSync(join(projectRoot, "app", "agent_python"))).toBe(true); + expect(existsSync(join(projectRoot, "app", "agent_python_minimal"))).toBe(true); expect(await Bun.file(envPath).text()).not.toContain(envKey); expect(io.stderr()).toContain(`removed '${envKey}' from ${ENV_LOCAL_RELATIVE_PATH}`); expect(io.stdout()).toContain("removed all resources from project"); diff --git a/src/handlers/project/shortcuts.ts b/src/handlers/project/shortcuts.ts index dba4142fe..d263ce69e 100644 --- a/src/handlers/project/shortcuts.ts +++ b/src/handlers/project/shortcuts.ts @@ -5,16 +5,11 @@ import { type Memory, } from "../../projectSchemas/memory"; import { InputValidationError } from "../../errors"; -import { ScaffoldRuntimeInputSchema, type ScaffoldRuntimeInput } from "./types"; +import { ScaffoldRuntimeInputSchema, type ModelProvider, type ScaffoldRuntimeInput } from "./types"; -export const MEMORY_SHORTCUTS = { - none: (_runtimeName: string) => undefined, - shortTerm: (runtimeName: string): Memory => ({ - name: `${runtimeName}Memory`, - eventExpiryDuration: 30, - strategies: [], - }), - longAndShortTerm: (runtimeName: string): Memory => ({ +/** The default memory that templates ship with. */ +export function getDefaultMemorySpec(runtimeName: string): Memory { + return { name: `${runtimeName}Memory`, eventExpiryDuration: 30, strategies: (["SEMANTIC", "USER_PREFERENCE", "SUMMARIZATION", "EPISODIC"] as const).map( @@ -26,37 +21,37 @@ export const MEMORY_SHORTCUTS = { }), }), ), - }), -} satisfies Record Memory | undefined>; - -export type MemoryShortcutName = keyof typeof MEMORY_SHORTCUTS; - -export const MEMORY_SHORTCUT_NAMES = Object.keys(MEMORY_SHORTCUTS) as unknown as readonly [ - MemoryShortcutName, - ...MemoryShortcutName[], -]; - -/** The default CodeZip runtime version for each language. */ -export const LANGUAGE_VERSION_DEFAULTS = { - Python: "PYTHON_3_14", - TypeScript: "NODE_22", -} as const satisfies Record< - ScaffoldRuntimeInput["language"], - NonNullable ->; + }; +} -type RuntimeTemplateShortcut = Omit & { - memory: MemoryShortcutName; +type RuntimeTemplateShortcut = { + runtimeName: string; + build: ScaffoldRuntimeInput["build"]; + language: ScaffoldRuntimeInput["language"]; + framework: ScaffoldRuntimeInput["framework"]; + protocol?: ScaffoldRuntimeInput["protocol"]; + modelProvider?: ModelProvider; + /** Ships with memory. */ + includesMemory: boolean; + /** Accepts --model-provider / --api-key overrides; Bedrock-only otherwise. */ + supportsModelProviderOverride: boolean; + runtimeVersion?: NonNullable; }; +/** + * The runtime templates. Only agent-python-strands offers a container build (its + * `-container` shortcut renders the same source with a Dockerfile); every other + * template is CodeZip-only. + */ export const RUNTIME_TEMPLATE_SHORTCUTS = { - "agent-python": { - runtimeName: "agent_python", + "agent-python-minimal": { + runtimeName: "agent_python_minimal", build: "CodeZip", language: "Python", framework: "none", modelProvider: "Bedrock", - memory: "none", + includesMemory: false, + supportsModelProviderOverride: false, runtimeVersion: "PYTHON_3_14", }, "agent-python-strands": { @@ -65,16 +60,18 @@ export const RUNTIME_TEMPLATE_SHORTCUTS = { language: "Python", framework: "strands", modelProvider: "Bedrock", - memory: "longAndShortTerm", + includesMemory: true, + supportsModelProviderOverride: true, runtimeVersion: "PYTHON_3_14", }, "agent-python-strands-container": { - runtimeName: "agent_python_strands", + runtimeName: "agent_python_strands_container", build: "Container", language: "Python", framework: "strands", modelProvider: "Bedrock", - memory: "longAndShortTerm", + includesMemory: true, + supportsModelProviderOverride: true, }, "agent-typescript-strands": { runtimeName: "agent_typescript_strands", @@ -82,7 +79,8 @@ export const RUNTIME_TEMPLATE_SHORTCUTS = { language: "TypeScript", framework: "strands", modelProvider: "Bedrock", - memory: "longAndShortTerm", + includesMemory: true, + supportsModelProviderOverride: false, runtimeVersion: "NODE_22", }, "mcp-python-fastmcp": { @@ -91,7 +89,8 @@ export const RUNTIME_TEMPLATE_SHORTCUTS = { language: "Python", framework: "none", protocol: "MCP", - memory: "none", + includesMemory: false, + supportsModelProviderOverride: false, runtimeVersion: "PYTHON_3_14", }, "a2a-python-strands": { @@ -101,7 +100,8 @@ export const RUNTIME_TEMPLATE_SHORTCUTS = { framework: "strands", protocol: "A2A", modelProvider: "Bedrock", - memory: "longAndShortTerm", + includesMemory: true, + supportsModelProviderOverride: false, runtimeVersion: "PYTHON_3_14", }, "agui-python-strands": { @@ -111,7 +111,8 @@ export const RUNTIME_TEMPLATE_SHORTCUTS = { framework: "strands", protocol: "AGUI", modelProvider: "Bedrock", - memory: "longAndShortTerm", + includesMemory: true, + supportsModelProviderOverride: false, runtimeVersion: "PYTHON_3_14", }, } as const satisfies Record; @@ -122,12 +123,21 @@ export const RUNTIME_TEMPLATE_SHORTCUT_NAMES = Object.keys( RUNTIME_TEMPLATE_SHORTCUTS, ) as unknown as readonly [RuntimeTemplateShortcutName, ...RuntimeTemplateShortcutName[]]; +/** The empty template scaffolds a project with no runtime and no harness. */ +export const EMPTY_TEMPLATE_NAME = "empty"; + +export type TemplateName = RuntimeTemplateShortcutName | typeof EMPTY_TEMPLATE_NAME; + +/** Every `--template` value: the runtime shortcuts plus the empty project template. */ +export const PROJECT_TEMPLATE_NAMES = [ + ...RUNTIME_TEMPLATE_SHORTCUT_NAMES, + EMPTY_TEMPLATE_NAME, +] as unknown as readonly [TemplateName, ...TemplateName[]]; + type RuntimeTemplateOverrides = { runtimeName?: string; - build?: ScaffoldRuntimeInput["build"]; - modelProvider?: ScaffoldRuntimeInput["modelProvider"]; + modelProvider?: ModelProvider; apiKey?: string; - memory?: MemoryShortcutName; }; export function resolveRuntimeTemplateShortcut( @@ -136,23 +146,19 @@ export function resolveRuntimeTemplateShortcut( ): ScaffoldRuntimeInput { const template: RuntimeTemplateShortcut = RUNTIME_TEMPLATE_SHORTCUTS[name]; const runtimeName = overrides?.runtimeName ?? template.runtimeName; - const build = overrides?.build ?? template.build; - const memoryShortcutName = overrides?.memory ?? template.memory; - const memory = MEMORY_SHORTCUTS[memoryShortcutName](runtimeName); const input = { runtimeName, - build, + build: template.build, language: template.language, framework: template.framework, protocol: template.protocol, - modelProvider: overrides?.modelProvider ?? template.modelProvider, + modelProvider: template.supportsModelProviderOverride + ? (overrides?.modelProvider ?? template.modelProvider) + : template.modelProvider, ...(overrides?.apiKey !== undefined && { apiKey: overrides.apiKey }), - ...(memory && { memory }), - runtimeVersion: - build === "CodeZip" - ? (template.runtimeVersion ?? LANGUAGE_VERSION_DEFAULTS[template.language]) - : undefined, + ...(template.includesMemory && { memory: getDefaultMemorySpec(runtimeName) }), + runtimeVersion: template.runtimeVersion, }; const result = ScaffoldRuntimeInputSchema.safeParse(input); diff --git a/src/handlers/project/types.ts b/src/handlers/project/types.ts index 17a77a912..b08179aa2 100644 --- a/src/handlers/project/types.ts +++ b/src/handlers/project/types.ts @@ -132,6 +132,12 @@ export type CreateProjectInput = CreateProjectInputBase & scaffoldRuntimeInput?: undefined; importBedrockAgent?: undefined; } + | { + /** The empty template: a project with neither a runtime nor a harness. */ + scaffoldRuntimeInput?: undefined; + scaffoldHarnessInput?: undefined; + importBedrockAgent?: undefined; + } ); /**