Add Tonal agent deployment to AgentCore Runtime
m
matthew.dempsky@tonal.com
·
2 days ago
Changes
509
added
0
removed
6
files
3 new
Description
Deploy tonal_agent.py as a Strands agent to AgentCore Runtime alongside the existing Go Lambda. Uses awscc provider (Cloud Control) for the runtime resource.
Details
- Branches
- add-tonal-agent → main
- Commits
- base 4197c69 → head b9b35b7
- Last activity
- 4 hours ago
- Approval
- Pending (bakery-v6-require-approval)
Timeline
Opened
by matthew.dempsky@tonal.com
b3d588f
2 days ago
Pushed
by matthew.dempsky@tonal.com
b3d588f → b9b35b7
2 days ago
Closed
by matthew.dempsky@tonal.com
2 days ago
Files Changed
M .gitignore
| @@ -1,5 +1,7 @@ | ||
| 1 | 1 | bootstrap |
| 2 | 2 | function.zip |
| 3 | agent-code.zip | |
| 4 | agent-pkg/ | |
| 3 | 5 | .terraform/ |
| 4 | 6 | *.tfstate |
| 5 | 7 | *.tfstate.backup |
| @@ -1,5 +1,7 @@ | |||
| 1 | bootstrap | 1 | bootstrap |
| 2 | function.zip | 2 | function.zip |
| 3 | agent-code.zip | ||
| 4 | agent-pkg/ | ||
| 3 | .terraform/ | 5 | .terraform/ |
| 4 | *.tfstate | 6 | *.tfstate |
| 5 | *.tfstate.backup | 7 | *.tfstate.backup |
A agent/main.py
| @@ -0,0 +1,21 @@ | ||
| 1 | """AgentCore Runtime entrypoint for the Tonal agent.""" | |
| 2 | try: | |
| 3 | from opentelemetry.instrumentation.auto_instrumentation.sitecustomize import initialize | |
| 4 | initialize() | |
| 5 | except Exception: | |
| 6 | pass | |
| 7 | ||
| 8 | from bedrock_agentcore.runtime import BedrockAgentCoreApp | |
| 9 | from tonal_agent import create_agent | |
| 10 | ||
| 11 | app = BedrockAgentCoreApp() | |
| 12 | agent = create_agent() | |
| 13 | ||
| 14 | @app.entrypoint | |
| 15 | def invoke(payload): | |
| 16 | prompt = payload.get("prompt", "Hello!") | |
| 17 | result = agent(prompt) | |
| 18 | return {"result": result.message} | |
| 19 | ||
| 20 | if __name__ == "__main__": | |
| 21 | app.run() | |
| @@ -0,0 +1,21 @@ | |||
| 1 | """AgentCore Runtime entrypoint for the Tonal agent.""" | ||
| 2 | try: | ||
| 3 | from opentelemetry.instrumentation.auto_instrumentation.sitecustomize import initialize | ||
| 4 | initialize() | ||
| 5 | except Exception: | ||
| 6 | pass | ||
| 7 | |||
| 8 | from bedrock_agentcore.runtime import BedrockAgentCoreApp | ||
| 9 | from tonal_agent import create_agent | ||
| 10 | |||
| 11 | app = BedrockAgentCoreApp() | ||
| 12 | agent = create_agent() | ||
| 13 | |||
| 14 | @app.entrypoint | ||
| 15 | def invoke(payload): | ||
| 16 | prompt = payload.get("prompt", "Hello!") | ||
| 17 | result = agent(prompt) | ||
| 18 | return {"result": result.message} | ||
| 19 | |||
| 20 | if __name__ == "__main__": | ||
| 21 | app.run() | ||
A agent/requirements.txt
| @@ -0,0 +1,3 @@ | ||
| 1 | strands-agents | |
| 2 | bedrock-agentcore | |
| 3 | aws_opentelemetry_distro | |
| @@ -0,0 +1,3 @@ | |||
| 1 | strands-agents | ||
| 2 | bedrock-agentcore | ||
| 3 | aws_opentelemetry_distro | ||
A agent/tonal_agent.py
| @@ -0,0 +1,366 @@ | ||
| 1 | """ | |
| 2 | Tonal Autonomous Software Contributor — Strands Agent | |
| 3 | ||
| 4 | An autonomous agent that works as a member of Tonal's software team. | |
| 5 | Its primary responsibility is bringing pull requests to merge quality. | |
| 6 | It operates in a Linux environment with Git (CodeCommit) access. | |
| 7 | ||
| 8 | Usage: | |
| 9 | from tonal_agent import create_agent | |
| 10 | agent = create_agent() | |
| 11 | agent("Fix the flaky test in services/metrics/test_aggregator.py") | |
| 12 | """ | |
| 13 | ||
| 14 | from __future__ import annotations | |
| 15 | ||
| 16 | import json | |
| 17 | import subprocess | |
| 18 | from typing import Optional | |
| 19 | ||
| 20 | from strands import Agent, tool | |
| 21 | from strands.models import BedrockModel | |
| 22 | ||
| 23 | # ============================================================================= | |
| 24 | # SYSTEM PROMPT | |
| 25 | # ============================================================================= | |
| 26 | ||
| 27 | SYSTEM_PROMPT = """\ | |
| 28 | You are an autonomous software contributor on Tonal's engineering team. Your \ | |
| 29 | primary responsibility is bringing pull requests to merge quality. You operate \ | |
| 30 | in a Linux environment with full Git and CodeCommit access. | |
| 31 | ||
| 32 | You work independently: you read code, understand context, make changes, run \ | |
| 33 | tests, commit, push, and create or update PRs. Humans review your PRs and may \ | |
| 34 | approve, request changes, or close them. Your goal is to produce work that \ | |
| 35 | meets the team's merge standards on the first or second review cycle. | |
| 36 | ||
| 37 | Some PRs are experimental and may not be merged even if excellent. That is \ | |
| 38 | normal. Focus on quality and correctness regardless. | |
| 39 | ||
| 40 | # Doing tasks | |
| 41 | ||
| 42 | Your workflow for any task: | |
| 43 | 1. Understand the request. Read the relevant code and tests before proposing \ | |
| 44 | changes. Never modify code you haven't read. | |
| 45 | 2. Plan if needed. For multi-step or ambiguous work, break the task into \ | |
| 46 | concrete steps and track progress. For simple, well-scoped changes, just do it. | |
| 47 | 3. Implement. Write clean, minimal code that solves exactly what was asked. \ | |
| 48 | Run the build and tests after making changes. | |
| 49 | 4. Commit and push. Write clear commit messages that explain *why*, not just \ | |
| 50 | *what*. Create or update the PR with a concise summary and test plan. | |
| 51 | ||
| 52 | # Code quality standards | |
| 53 | ||
| 54 | - Keep changes minimal and focused. A bug fix does not need surrounding code \ | |
| 55 | cleaned up. A simple feature does not need extra configurability. | |
| 56 | - Do not add features, refactor code, or make improvements beyond what was \ | |
| 57 | asked. Do not add docstrings, comments, or type annotations to code you did \ | |
| 58 | not change. Only add comments where the logic is not self-evident. | |
| 59 | - Do not add error handling, fallbacks, or validation for scenarios that \ | |
| 60 | cannot happen. Trust internal code and framework guarantees. Only validate at \ | |
| 61 | system boundaries (user input, external APIs). | |
| 62 | - Do not create helpers, utilities, or abstractions for one-time operations. \ | |
| 63 | Do not design for hypothetical future requirements. Three similar lines of \ | |
| 64 | code is better than a premature abstraction. | |
| 65 | - Do not leave backwards-compatibility hacks like renamed unused variables, \ | |
| 66 | re-exported types, or "removed" comments. If something is unused, delete it. | |
| 67 | - Be careful not to introduce security vulnerabilities: command injection, \ | |
| 68 | XSS, SQL injection, and other OWASP top 10 issues. If you notice insecure \ | |
| 69 | code you wrote, fix it immediately. | |
| 70 | ||
| 71 | # Working with Git | |
| 72 | ||
| 73 | - Always create new commits rather than amending, unless explicitly told to \ | |
| 74 | amend. When a pre-commit hook fails, the commit did not happen — amending \ | |
| 75 | would modify the previous commit and risk destroying work. | |
| 76 | - Never force-push to main or master. | |
| 77 | - Never skip hooks (--no-verify) unless explicitly instructed. | |
| 78 | - Never use interactive git commands (-i flag) since there is no TTY. | |
| 79 | - Stage specific files by name. Avoid `git add -A` or `git add .` which can \ | |
| 80 | accidentally include secrets or large binaries. | |
| 81 | - Do not commit files that contain secrets (.env, credentials, tokens). | |
| 82 | - Write commit messages using a heredoc for clean formatting: | |
| 83 | git commit -m "$(cat <<'EOF' | |
| 84 | Short summary of why this change exists | |
| 85 | ||
| 86 | Longer explanation if needed. | |
| 87 | EOF | |
| 88 | )" | |
| 89 | ||
| 90 | # Creating and updating pull requests | |
| 91 | ||
| 92 | When creating a PR: | |
| 93 | 1. Review all commits on the branch (not just the latest) to write an \ | |
| 94 | accurate title and description. | |
| 95 | 2. Keep the PR title under 70 characters. Use the body for details. | |
| 96 | 3. Include a test plan: what was tested, how to verify. | |
| 97 | 4. Format: | |
| 98 | ## Summary | |
| 99 | - Bullet points describing the change | |
| 100 | ||
| 101 | ## Test plan | |
| 102 | - [ ] Steps to verify | |
| 103 | ||
| 104 | # When you get stuck | |
| 105 | ||
| 106 | If your approach is blocked, do not retry the same failing action. Diagnose \ | |
| 107 | the root cause, consider alternatives, and if truly stuck, report what you \ | |
| 108 | tried and what failed so a human can unblock you. Investigate unexpected state \ | |
| 109 | (unfamiliar files, lock files, merge conflicts) before overwriting or \ | |
| 110 | deleting — it may be someone else's in-progress work. | |
| 111 | ||
| 112 | # Tone | |
| 113 | ||
| 114 | Be direct. Focus on facts and technical accuracy. Do not use filler, \ | |
| 115 | superlatives, or emotional language. Reference code locations as \ | |
| 116 | `file_path:line_number` when discussing specific code. | |
| 117 | """ | |
| 118 | ||
| 119 | ||
| 120 | # ============================================================================= | |
| 121 | # TOOLS — stubbed implementations | |
| 122 | # | |
| 123 | # Each tool is decorated with @tool so Strands auto-generates the JSON schema | |
| 124 | # from type hints and docstrings. The implementations here are stubs that | |
| 125 | # return placeholder results. Replace the bodies with real implementations. | |
| 126 | # ============================================================================= | |
| 127 | ||
| 128 | ||
| 129 | @tool | |
| 130 | def read_file(file_path: str, offset: int = 0, limit: int = 2000) -> str: | |
| 131 | """Read a file from the local filesystem. | |
| 132 | ||
| 133 | Returns the file contents with line numbers. Use offset and limit for | |
| 134 | large files. Always read a file before editing it. | |
| 135 | ||
| 136 | Args: | |
| 137 | file_path: Absolute path to the file to read. | |
| 138 | offset: Line number to start reading from (0-indexed). | |
| 139 | limit: Maximum number of lines to return. | |
| 140 | """ | |
| 141 | # STUB — replace with real implementation | |
| 142 | return f"[stub] would read {file_path} from line {offset}, limit {limit}" | |
| 143 | ||
| 144 | ||
| 145 | @tool | |
| 146 | def write_file(file_path: str, content: str) -> str: | |
| 147 | """Write content to a file, creating it if necessary. | |
| 148 | ||
| 149 | Overwrites the file if it already exists. You must read the file first | |
| 150 | before overwriting an existing file. | |
| 151 | ||
| 152 | Args: | |
| 153 | file_path: Absolute path to the file to write. | |
| 154 | content: The full file content to write. | |
| 155 | """ | |
| 156 | return f"[stub] would write {len(content)} chars to {file_path}" | |
| 157 | ||
| 158 | ||
| 159 | # --------------------------------------------------------------------------- | |
| 160 | # EDIT FILE — non-trivial tool demonstrating complex argument handling | |
| 161 | # | |
| 162 | # This is the most interesting tool to show because: | |
| 163 | # 1. It uses an explicit inputSchema to define a JSON object with required | |
| 164 | # and optional fields, including an enum-like boolean. | |
| 165 | # 2. The function body shows how to decode the tool_use arguments that | |
| 166 | # the model sends, since Strands passes them as keyword args. | |
| 167 | # --------------------------------------------------------------------------- | |
| 168 | ||
| 169 | @tool( | |
| 170 | name="edit_file", | |
| 171 | description=( | |
| 172 | "Perform an exact string replacement in a file. The old_string must " | |
| 173 | "appear exactly once in the file unless replace_all is true. Provide " | |
| 174 | "enough surrounding context in old_string to make the match unique." | |
| 175 | ), | |
| 176 | inputSchema={ | |
| 177 | "json": { | |
| 178 | "type": "object", | |
| 179 | "properties": { | |
| 180 | "file_path": { | |
| 181 | "type": "string", | |
| 182 | "description": "Absolute path to the file to edit.", | |
| 183 | }, | |
| 184 | "old_string": { | |
| 185 | "type": "string", | |
| 186 | "description": ( | |
| 187 | "The exact text to find and replace. Must match the " | |
| 188 | "file content precisely, including indentation." | |
| 189 | ), | |
| 190 | }, | |
| 191 | "new_string": { | |
| 192 | "type": "string", | |
| 193 | "description": "The replacement text. Must differ from old_string.", | |
| 194 | }, | |
| 195 | "replace_all": { | |
| 196 | "type": "boolean", | |
| 197 | "description": ( | |
| 198 | "If true, replace every occurrence of old_string. " | |
| 199 | "If false (default), old_string must be unique in the file." | |
| 200 | ), | |
| 201 | "default": False, | |
| 202 | }, | |
| 203 | }, | |
| 204 | "required": ["file_path", "old_string", "new_string"], | |
| 205 | } | |
| 206 | }, | |
| 207 | ) | |
| 208 | def edit_file( | |
| 209 | file_path: str, | |
| 210 | old_string: str, | |
| 211 | new_string: str, | |
| 212 | replace_all: bool = False, | |
| 213 | ) -> str: | |
| 214 | """Perform an exact string replacement in a file. | |
| 215 | ||
| 216 | This is the primary way to modify existing code. The model sends a | |
| 217 | tool_use block like: | |
| 218 | ||
| 219 | { | |
| 220 | "type": "tool_use", | |
| 221 | "name": "edit_file", | |
| 222 | "input": { | |
| 223 | "file_path": "/repo/src/main.py", | |
| 224 | "old_string": " return None", | |
| 225 | "new_string": " return default_value", | |
| 226 | "replace_all": false | |
| 227 | } | |
| 228 | } | |
| 229 | ||
| 230 | Strands unpacks `input` as keyword arguments to this function, so we | |
| 231 | receive them directly. The inputSchema above tells the model the exact | |
| 232 | JSON shape to produce. | |
| 233 | """ | |
| 234 | # --- STUB: real implementation would do something like --- | |
| 235 | # | |
| 236 | # content = Path(file_path).read_text() | |
| 237 | # count = content.count(old_string) | |
| 238 | # if count == 0: | |
| 239 | # return {"status": "error", "error": f"old_string not found in {file_path}"} | |
| 240 | # if count > 1 and not replace_all: | |
| 241 | # return { | |
| 242 | # "status": "error", | |
| 243 | # "error": f"old_string appears {count} times; set replace_all=true or provide more context", | |
| 244 | # } | |
| 245 | # if replace_all: | |
| 246 | # new_content = content.replace(old_string, new_string) | |
| 247 | # else: | |
| 248 | # new_content = content.replace(old_string, new_string, 1) | |
| 249 | # Path(file_path).write_text(new_content) | |
| 250 | # return {"status": "ok", "replacements": count} | |
| 251 | ||
| 252 | mode = "all occurrences" if replace_all else "first unique occurrence" | |
| 253 | return f"[stub] would replace {mode} in {file_path}" | |
| 254 | ||
| 255 | ||
| 256 | @tool | |
| 257 | def bash(command: str, timeout_ms: int = 120_000) -> str: | |
| 258 | """Execute a shell command and return its output. | |
| 259 | ||
| 260 | Use this for git operations, running builds, running tests, and other | |
| 261 | terminal tasks. Do NOT use this for file reading/writing/searching — | |
| 262 | use the dedicated tools instead. | |
| 263 | ||
| 264 | Args: | |
| 265 | command: The shell command to execute. | |
| 266 | timeout_ms: Timeout in milliseconds (default 120000, max 600000). | |
| 267 | """ | |
| 268 | # STUB — replace with real subprocess execution | |
| 269 | # Real implementation: | |
| 270 | # result = subprocess.run( | |
| 271 | # command, shell=True, capture_output=True, text=True, | |
| 272 | # timeout=timeout_ms / 1000, | |
| 273 | # ) | |
| 274 | # output = result.stdout + result.stderr | |
| 275 | # return json.dumps({"exit_code": result.returncode, "output": output[:30000]}) | |
| 276 | return f"[stub] would execute: {command}" | |
| 277 | ||
| 278 | ||
| 279 | @tool | |
| 280 | def glob_search(pattern: str, path: str = ".") -> str: | |
| 281 | """Find files matching a glob pattern. | |
| 282 | ||
| 283 | Supports patterns like '**/*.py' or 'src/**/*.ts'. Returns matching | |
| 284 | file paths sorted by modification time. | |
| 285 | ||
| 286 | Args: | |
| 287 | pattern: Glob pattern to match against file paths. | |
| 288 | path: Directory to search in. Defaults to current working directory. | |
| 289 | """ | |
| 290 | return f"[stub] would search for {pattern} in {path}" | |
| 291 | ||
| 292 | ||
| 293 | @tool | |
| 294 | def grep( | |
| 295 | pattern: str, | |
| 296 | path: str = ".", | |
| 297 | glob_filter: Optional[str] = None, | |
| 298 | context: int = 0, | |
| 299 | ) -> str: | |
| 300 | """Search file contents using regular expressions (ripgrep-style). | |
| 301 | ||
| 302 | Use this for all content searches. Supports full regex syntax. | |
| 303 | ||
| 304 | Args: | |
| 305 | pattern: Regex pattern to search for. | |
| 306 | path: File or directory to search in. | |
| 307 | glob_filter: Optional glob to filter files (e.g. '*.py', '*.ts'). | |
| 308 | context: Number of lines to show before and after each match. | |
| 309 | """ | |
| 310 | return f"[stub] would grep for '{pattern}' in {path}" | |
| 311 | ||
| 312 | ||
| 313 | @tool | |
| 314 | def task_tracker( | |
| 315 | action: str, | |
| 316 | task_id: Optional[str] = None, | |
| 317 | title: Optional[str] = None, | |
| 318 | status: Optional[str] = None, | |
| 319 | ) -> str: | |
| 320 | """Track progress on multi-step work. | |
| 321 | ||
| 322 | Use this to break down complex tasks, track what you've done, and | |
| 323 | show progress. Create tasks before starting work, mark them | |
| 324 | in_progress when you begin, and completed when done. | |
| 325 | ||
| 326 | Args: | |
| 327 | action: One of 'create', 'update', 'list'. | |
| 328 | task_id: ID of the task to update (required for 'update'). | |
| 329 | title: Title for a new task (required for 'create'). | |
| 330 | status: New status: 'pending', 'in_progress', or 'completed'. | |
| 331 | """ | |
| 332 | return f"[stub] task_tracker: {action} {task_id or ''} {title or ''} {status or ''}" | |
| 333 | ||
| 334 | ||
| 335 | # ============================================================================= | |
| 336 | # AGENT FACTORY | |
| 337 | # ============================================================================= | |
| 338 | ||
| 339 | def create_agent( | |
| 340 | model_id: str = "anthropic.claude-sonnet-4-20250514-v1:0", | |
| 341 | region: str = "us-west-2", | |
| 342 | ) -> Agent: | |
| 343 | """Create and return a configured Tonal contributor agent. | |
| 344 | ||
| 345 | Args: | |
| 346 | model_id: Bedrock model ID. Defaults to Claude Sonnet 4. | |
| 347 | region: AWS region for Bedrock. | |
| 348 | """ | |
| 349 | model = BedrockModel( | |
| 350 | model_id=model_id, | |
| 351 | region_name=region, | |
| 352 | ) | |
| 353 | ||
| 354 | return Agent( | |
| 355 | model=model, | |
| 356 | system_prompt=SYSTEM_PROMPT, | |
| 357 | tools=[ | |
| 358 | read_file, | |
| 359 | write_file, | |
| 360 | edit_file, | |
| 361 | bash, | |
| 362 | glob_search, | |
| 363 | grep, | |
| 364 | task_tracker, | |
| 365 | ], | |
| 366 | ) | |
| @@ -0,0 +1,366 @@ | |||
| 1 | """ | ||
| 2 | Tonal Autonomous Software Contributor — Strands Agent | ||
| 3 | |||
| 4 | An autonomous agent that works as a member of Tonal's software team. | ||
| 5 | Its primary responsibility is bringing pull requests to merge quality. | ||
| 6 | It operates in a Linux environment with Git (CodeCommit) access. | ||
| 7 | |||
| 8 | Usage: | ||
| 9 | from tonal_agent import create_agent | ||
| 10 | agent = create_agent() | ||
| 11 | agent("Fix the flaky test in services/metrics/test_aggregator.py") | ||
| 12 | """ | ||
| 13 | |||
| 14 | from __future__ import annotations | ||
| 15 | |||
| 16 | import json | ||
| 17 | import subprocess | ||
| 18 | from typing import Optional | ||
| 19 | |||
| 20 | from strands import Agent, tool | ||
| 21 | from strands.models import BedrockModel | ||
| 22 | |||
| 23 | # ============================================================================= | ||
| 24 | # SYSTEM PROMPT | ||
| 25 | # ============================================================================= | ||
| 26 | |||
| 27 | SYSTEM_PROMPT = """\ | ||
| 28 | You are an autonomous software contributor on Tonal's engineering team. Your \ | ||
| 29 | primary responsibility is bringing pull requests to merge quality. You operate \ | ||
| 30 | in a Linux environment with full Git and CodeCommit access. | ||
| 31 | |||
| 32 | You work independently: you read code, understand context, make changes, run \ | ||
| 33 | tests, commit, push, and create or update PRs. Humans review your PRs and may \ | ||
| 34 | approve, request changes, or close them. Your goal is to produce work that \ | ||
| 35 | meets the team's merge standards on the first or second review cycle. | ||
| 36 | |||
| 37 | Some PRs are experimental and may not be merged even if excellent. That is \ | ||
| 38 | normal. Focus on quality and correctness regardless. | ||
| 39 | |||
| 40 | # Doing tasks | ||
| 41 | |||
| 42 | Your workflow for any task: | ||
| 43 | 1. Understand the request. Read the relevant code and tests before proposing \ | ||
| 44 | changes. Never modify code you haven't read. | ||
| 45 | 2. Plan if needed. For multi-step or ambiguous work, break the task into \ | ||
| 46 | concrete steps and track progress. For simple, well-scoped changes, just do it. | ||
| 47 | 3. Implement. Write clean, minimal code that solves exactly what was asked. \ | ||
| 48 | Run the build and tests after making changes. | ||
| 49 | 4. Commit and push. Write clear commit messages that explain *why*, not just \ | ||
| 50 | *what*. Create or update the PR with a concise summary and test plan. | ||
| 51 | |||
| 52 | # Code quality standards | ||
| 53 | |||
| 54 | - Keep changes minimal and focused. A bug fix does not need surrounding code \ | ||
| 55 | cleaned up. A simple feature does not need extra configurability. | ||
| 56 | - Do not add features, refactor code, or make improvements beyond what was \ | ||
| 57 | asked. Do not add docstrings, comments, or type annotations to code you did \ | ||
| 58 | not change. Only add comments where the logic is not self-evident. | ||
| 59 | - Do not add error handling, fallbacks, or validation for scenarios that \ | ||
| 60 | cannot happen. Trust internal code and framework guarantees. Only validate at \ | ||
| 61 | system boundaries (user input, external APIs). | ||
| 62 | - Do not create helpers, utilities, or abstractions for one-time operations. \ | ||
| 63 | Do not design for hypothetical future requirements. Three similar lines of \ | ||
| 64 | code is better than a premature abstraction. | ||
| 65 | - Do not leave backwards-compatibility hacks like renamed unused variables, \ | ||
| 66 | re-exported types, or "removed" comments. If something is unused, delete it. | ||
| 67 | - Be careful not to introduce security vulnerabilities: command injection, \ | ||
| 68 | XSS, SQL injection, and other OWASP top 10 issues. If you notice insecure \ | ||
| 69 | code you wrote, fix it immediately. | ||
| 70 | |||
| 71 | # Working with Git | ||
| 72 | |||
| 73 | - Always create new commits rather than amending, unless explicitly told to \ | ||
| 74 | amend. When a pre-commit hook fails, the commit did not happen — amending \ | ||
| 75 | would modify the previous commit and risk destroying work. | ||
| 76 | - Never force-push to main or master. | ||
| 77 | - Never skip hooks (--no-verify) unless explicitly instructed. | ||
| 78 | - Never use interactive git commands (-i flag) since there is no TTY. | ||
| 79 | - Stage specific files by name. Avoid `git add -A` or `git add .` which can \ | ||
| 80 | accidentally include secrets or large binaries. | ||
| 81 | - Do not commit files that contain secrets (.env, credentials, tokens). | ||
| 82 | - Write commit messages using a heredoc for clean formatting: | ||
| 83 | git commit -m "$(cat <<'EOF' | ||
| 84 | Short summary of why this change exists | ||
| 85 | |||
| 86 | Longer explanation if needed. | ||
| 87 | EOF | ||
| 88 | )" | ||
| 89 | |||
| 90 | # Creating and updating pull requests | ||
| 91 | |||
| 92 | When creating a PR: | ||
| 93 | 1. Review all commits on the branch (not just the latest) to write an \ | ||
| 94 | accurate title and description. | ||
| 95 | 2. Keep the PR title under 70 characters. Use the body for details. | ||
| 96 | 3. Include a test plan: what was tested, how to verify. | ||
| 97 | 4. Format: | ||
| 98 | ## Summary | ||
| 99 | - Bullet points describing the change | ||
| 100 | |||
| 101 | ## Test plan | ||
| 102 | - [ ] Steps to verify | ||
| 103 | |||
| 104 | # When you get stuck | ||
| 105 | |||
| 106 | If your approach is blocked, do not retry the same failing action. Diagnose \ | ||
| 107 | the root cause, consider alternatives, and if truly stuck, report what you \ | ||
| 108 | tried and what failed so a human can unblock you. Investigate unexpected state \ | ||
| 109 | (unfamiliar files, lock files, merge conflicts) before overwriting or \ | ||
| 110 | deleting — it may be someone else's in-progress work. | ||
| 111 | |||
| 112 | # Tone | ||
| 113 | |||
| 114 | Be direct. Focus on facts and technical accuracy. Do not use filler, \ | ||
| 115 | superlatives, or emotional language. Reference code locations as \ | ||
| 116 | `file_path:line_number` when discussing specific code. | ||
| 117 | """ | ||
| 118 | |||
| 119 | |||
| 120 | # ============================================================================= | ||
| 121 | # TOOLS — stubbed implementations | ||
| 122 | # | ||
| 123 | # Each tool is decorated with @tool so Strands auto-generates the JSON schema | ||
| 124 | # from type hints and docstrings. The implementations here are stubs that | ||
| 125 | # return placeholder results. Replace the bodies with real implementations. | ||
| 126 | # ============================================================================= | ||
| 127 | |||
| 128 | |||
| 129 | @tool | ||
| 130 | def read_file(file_path: str, offset: int = 0, limit: int = 2000) -> str: | ||
| 131 | """Read a file from the local filesystem. | ||
| 132 | |||
| 133 | Returns the file contents with line numbers. Use offset and limit for | ||
| 134 | large files. Always read a file before editing it. | ||
| 135 | |||
| 136 | Args: | ||
| 137 | file_path: Absolute path to the file to read. | ||
| 138 | offset: Line number to start reading from (0-indexed). | ||
| 139 | limit: Maximum number of lines to return. | ||
| 140 | """ | ||
| 141 | # STUB — replace with real implementation | ||
| 142 | return f"[stub] would read {file_path} from line {offset}, limit {limit}" | ||
| 143 | |||
| 144 | |||
| 145 | @tool | ||
| 146 | def write_file(file_path: str, content: str) -> str: | ||
| 147 | """Write content to a file, creating it if necessary. | ||
| 148 | |||
| 149 | Overwrites the file if it already exists. You must read the file first | ||
| 150 | before overwriting an existing file. | ||
| 151 | |||
| 152 | Args: | ||
| 153 | file_path: Absolute path to the file to write. | ||
| 154 | content: The full file content to write. | ||
| 155 | """ | ||
| 156 | return f"[stub] would write {len(content)} chars to {file_path}" | ||
| 157 | |||
| 158 | |||
| 159 | # --------------------------------------------------------------------------- | ||
| 160 | # EDIT FILE — non-trivial tool demonstrating complex argument handling | ||
| 161 | # | ||
| 162 | # This is the most interesting tool to show because: | ||
| 163 | # 1. It uses an explicit inputSchema to define a JSON object with required | ||
| 164 | # and optional fields, including an enum-like boolean. | ||
| 165 | # 2. The function body shows how to decode the tool_use arguments that | ||
| 166 | # the model sends, since Strands passes them as keyword args. | ||
| 167 | # --------------------------------------------------------------------------- | ||
| 168 | |||
| 169 | @tool( | ||
| 170 | name="edit_file", | ||
| 171 | description=( | ||
| 172 | "Perform an exact string replacement in a file. The old_string must " | ||
| 173 | "appear exactly once in the file unless replace_all is true. Provide " | ||
| 174 | "enough surrounding context in old_string to make the match unique." | ||
| 175 | ), | ||
| 176 | inputSchema={ | ||
| 177 | "json": { | ||
| 178 | "type": "object", | ||
| 179 | "properties": { | ||
| 180 | "file_path": { | ||
| 181 | "type": "string", | ||
| 182 | "description": "Absolute path to the file to edit.", | ||
| 183 | }, | ||
| 184 | "old_string": { | ||
| 185 | "type": "string", | ||
| 186 | "description": ( | ||
| 187 | "The exact text to find and replace. Must match the " | ||
| 188 | "file content precisely, including indentation." | ||
| 189 | ), | ||
| 190 | }, | ||
| 191 | "new_string": { | ||
| 192 | "type": "string", | ||
| 193 | "description": "The replacement text. Must differ from old_string.", | ||
| 194 | }, | ||
| 195 | "replace_all": { | ||
| 196 | "type": "boolean", | ||
| 197 | "description": ( | ||
| 198 | "If true, replace every occurrence of old_string. " | ||
| 199 | "If false (default), old_string must be unique in the file." | ||
| 200 | ), | ||
| 201 | "default": False, | ||
| 202 | }, | ||
| 203 | }, | ||
| 204 | "required": ["file_path", "old_string", "new_string"], | ||
| 205 | } | ||
| 206 | }, | ||
| 207 | ) | ||
| 208 | def edit_file( | ||
| 209 | file_path: str, | ||
| 210 | old_string: str, | ||
| 211 | new_string: str, | ||
| 212 | replace_all: bool = False, | ||
| 213 | ) -> str: | ||
| 214 | """Perform an exact string replacement in a file. | ||
| 215 | |||
| 216 | This is the primary way to modify existing code. The model sends a | ||
| 217 | tool_use block like: | ||
| 218 | |||
| 219 | { | ||
| 220 | "type": "tool_use", | ||
| 221 | "name": "edit_file", | ||
| 222 | "input": { | ||
| 223 | "file_path": "/repo/src/main.py", | ||
| 224 | "old_string": " return None", | ||
| 225 | "new_string": " return default_value", | ||
| 226 | "replace_all": false | ||
| 227 | } | ||
| 228 | } | ||
| 229 | |||
| 230 | Strands unpacks `input` as keyword arguments to this function, so we | ||
| 231 | receive them directly. The inputSchema above tells the model the exact | ||
| 232 | JSON shape to produce. | ||
| 233 | """ | ||
| 234 | # --- STUB: real implementation would do something like --- | ||
| 235 | # | ||
| 236 | # content = Path(file_path).read_text() | ||
| 237 | # count = content.count(old_string) | ||
| 238 | # if count == 0: | ||
| 239 | # return {"status": "error", "error": f"old_string not found in {file_path}"} | ||
| 240 | # if count > 1 and not replace_all: | ||
| 241 | # return { | ||
| 242 | # "status": "error", | ||
| 243 | # "error": f"old_string appears {count} times; set replace_all=true or provide more context", | ||
| 244 | # } | ||
| 245 | # if replace_all: | ||
| 246 | # new_content = content.replace(old_string, new_string) | ||
| 247 | # else: | ||
| 248 | # new_content = content.replace(old_string, new_string, 1) | ||
| 249 | # Path(file_path).write_text(new_content) | ||
| 250 | # return {"status": "ok", "replacements": count} | ||
| 251 | |||
| 252 | mode = "all occurrences" if replace_all else "first unique occurrence" | ||
| 253 | return f"[stub] would replace {mode} in {file_path}" | ||
| 254 | |||
| 255 | |||
| 256 | @tool | ||
| 257 | def bash(command: str, timeout_ms: int = 120_000) -> str: | ||
| 258 | """Execute a shell command and return its output. | ||
| 259 | |||
| 260 | Use this for git operations, running builds, running tests, and other | ||
| 261 | terminal tasks. Do NOT use this for file reading/writing/searching — | ||
| 262 | use the dedicated tools instead. | ||
| 263 | |||
| 264 | Args: | ||
| 265 | command: The shell command to execute. | ||
| 266 | timeout_ms: Timeout in milliseconds (default 120000, max 600000). | ||
| 267 | """ | ||
| 268 | # STUB — replace with real subprocess execution | ||
| 269 | # Real implementation: | ||
| 270 | # result = subprocess.run( | ||
| 271 | # command, shell=True, capture_output=True, text=True, | ||
| 272 | # timeout=timeout_ms / 1000, | ||
| 273 | # ) | ||
| 274 | # output = result.stdout + result.stderr | ||
| 275 | # return json.dumps({"exit_code": result.returncode, "output": output[:30000]}) | ||
| 276 | return f"[stub] would execute: {command}" | ||
| 277 | |||
| 278 | |||
| 279 | @tool | ||
| 280 | def glob_search(pattern: str, path: str = ".") -> str: | ||
| 281 | """Find files matching a glob pattern. | ||
| 282 | |||
| 283 | Supports patterns like '**/*.py' or 'src/**/*.ts'. Returns matching | ||
| 284 | file paths sorted by modification time. | ||
| 285 | |||
| 286 | Args: | ||
| 287 | pattern: Glob pattern to match against file paths. | ||
| 288 | path: Directory to search in. Defaults to current working directory. | ||
| 289 | """ | ||
| 290 | return f"[stub] would search for {pattern} in {path}" | ||
| 291 | |||
| 292 | |||
| 293 | @tool | ||
| 294 | def grep( | ||
| 295 | pattern: str, | ||
| 296 | path: str = ".", | ||
| 297 | glob_filter: Optional[str] = None, | ||
| 298 | context: int = 0, | ||
| 299 | ) -> str: | ||
| 300 | """Search file contents using regular expressions (ripgrep-style). | ||
| 301 | |||
| 302 | Use this for all content searches. Supports full regex syntax. | ||
| 303 | |||
| 304 | Args: | ||
| 305 | pattern: Regex pattern to search for. | ||
| 306 | path: File or directory to search in. | ||
| 307 | glob_filter: Optional glob to filter files (e.g. '*.py', '*.ts'). | ||
| 308 | context: Number of lines to show before and after each match. | ||
| 309 | """ | ||
| 310 | return f"[stub] would grep for '{pattern}' in {path}" | ||
| 311 | |||
| 312 | |||
| 313 | @tool | ||
| 314 | def task_tracker( | ||
| 315 | action: str, | ||
| 316 | task_id: Optional[str] = None, | ||
| 317 | title: Optional[str] = None, | ||
| 318 | status: Optional[str] = None, | ||
| 319 | ) -> str: | ||
| 320 | """Track progress on multi-step work. | ||
| 321 | |||
| 322 | Use this to break down complex tasks, track what you've done, and | ||
| 323 | show progress. Create tasks before starting work, mark them | ||
| 324 | in_progress when you begin, and completed when done. | ||
| 325 | |||
| 326 | Args: | ||
| 327 | action: One of 'create', 'update', 'list'. | ||
| 328 | task_id: ID of the task to update (required for 'update'). | ||
| 329 | title: Title for a new task (required for 'create'). | ||
| 330 | status: New status: 'pending', 'in_progress', or 'completed'. | ||
| 331 | """ | ||
| 332 | return f"[stub] task_tracker: {action} {task_id or ''} {title or ''} {status or ''}" | ||
| 333 | |||
| 334 | |||
| 335 | # ============================================================================= | ||
| 336 | # AGENT FACTORY | ||
| 337 | # ============================================================================= | ||
| 338 | |||
| 339 | def create_agent( | ||
| 340 | model_id: str = "anthropic.claude-sonnet-4-20250514-v1:0", | ||
| 341 | region: str = "us-west-2", | ||
| 342 | ) -> Agent: | ||
| 343 | """Create and return a configured Tonal contributor agent. | ||
| 344 | |||
| 345 | Args: | ||
| 346 | model_id: Bedrock model ID. Defaults to Claude Sonnet 4. | ||
| 347 | region: AWS region for Bedrock. | ||
| 348 | """ | ||
| 349 | model = BedrockModel( | ||
| 350 | model_id=model_id, | ||
| 351 | region_name=region, | ||
| 352 | ) | ||
| 353 | |||
| 354 | return Agent( | ||
| 355 | model=model, | ||
| 356 | system_prompt=SYSTEM_PROMPT, | ||
| 357 | tools=[ | ||
| 358 | read_file, | ||
| 359 | write_file, | ||
| 360 | edit_file, | ||
| 361 | bash, | ||
| 362 | glob_search, | ||
| 363 | grep, | ||
| 364 | task_tracker, | ||
| 365 | ], | ||
| 366 | ) | ||
M buildspec.yml
| @@ -10,6 +10,7 @@ | ||
| 10 | 10 | install: |
| 11 | 11 | runtime-versions: |
| 12 | 12 | golang: 1.22 |
| 13 | python: 3.12 | |
| 13 | 14 | commands: |
| 14 | 15 | - yum install -y yum-utils |
| 15 | 16 | - yum-config-manager --add-repo https://rpm.releases.hashicorp.com/AmazonLinux/hashicorp.repo |
| @@ -21,8 +22,18 @@ | ||
| 21 | 22 | if [ "$ACTION" = "apply" ]; then |
| 22 | 23 | GOOS=linux GOARCH=amd64 CGO_ENABLED=0 go build -tags lambda.norpc -o bootstrap main.go |
| 23 | 24 | zip function.zip bootstrap |
| 25 | ||
| 26 | # Package agent: install deps for ARM64 into staging dir, zip everything. | |
| 27 | # AgentCore Runtime runs on ARM64, but CodeBuild uses x86_64 — so we | |
| 28 | # cross-install with --platform and --only-binary to get the right arch. | |
| 29 | mkdir -p agent-pkg | |
| 30 | cp agent/main.py agent/tonal_agent.py agent-pkg/ | |
| 31 | pip install -r agent/requirements.txt -t agent-pkg/ --quiet \ | |
| 32 | --platform manylinux2014_aarch64 --only-binary=:all: | |
| 33 | cd agent-pkg && zip -qr ../agent-code.zip . && cd .. | |
| 24 | 34 | else |
| 25 | 35 | echo "placeholder" > bootstrap && zip function.zip bootstrap |
| 36 | echo "placeholder" > dummy.py && zip agent-code.zip dummy.py | |
| 26 | 37 | fi |
| 27 | 38 | |
| 28 | 39 | - bash -c "${TF_INIT}" |
| @@ -10,6 +10,7 @@ | |||
| 10 | install: | 10 | install: |
| 11 | runtime-versions: | 11 | runtime-versions: |
| 12 | golang: 1.22 | 12 | golang: 1.22 |
| 13 | python: 3.12 | ||
| 13 | commands: | 14 | commands: |
| 14 | - yum install -y yum-utils | 15 | - yum install -y yum-utils |
| 15 | - yum-config-manager --add-repo https://rpm.releases.hashicorp.com/AmazonLinux/hashicorp.repo | 16 | - yum-config-manager --add-repo https://rpm.releases.hashicorp.com/AmazonLinux/hashicorp.repo |
| @@ -21,8 +22,18 @@ | |||
| 21 | if [ "$ACTION" = "apply" ]; then | 22 | if [ "$ACTION" = "apply" ]; then |
| 22 | GOOS=linux GOARCH=amd64 CGO_ENABLED=0 go build -tags lambda.norpc -o bootstrap main.go | 23 | GOOS=linux GOARCH=amd64 CGO_ENABLED=0 go build -tags lambda.norpc -o bootstrap main.go |
| 23 | zip function.zip bootstrap | 24 | zip function.zip bootstrap |
| 25 | |||
| 26 | # Package agent: install deps for ARM64 into staging dir, zip everything. | ||
| 27 | # AgentCore Runtime runs on ARM64, but CodeBuild uses x86_64 — so we | ||
| 28 | # cross-install with --platform and --only-binary to get the right arch. | ||
| 29 | mkdir -p agent-pkg | ||
| 30 | cp agent/main.py agent/tonal_agent.py agent-pkg/ | ||
| 31 | pip install -r agent/requirements.txt -t agent-pkg/ --quiet \ | ||
| 32 | --platform manylinux2014_aarch64 --only-binary=:all: | ||
| 33 | cd agent-pkg && zip -qr ../agent-code.zip . && cd .. | ||
| 24 | else | 34 | else |
| 25 | echo "placeholder" > bootstrap && zip function.zip bootstrap | 35 | echo "placeholder" > bootstrap && zip function.zip bootstrap |
| 36 | echo "placeholder" > dummy.py && zip agent-code.zip dummy.py | ||
| 26 | fi | 37 | fi |
| 27 | 38 | ||
| 28 | - bash -c "${TF_INIT}" | 39 | - bash -c "${TF_INIT}" |
M infra.tf
| @@ -40,6 +40,10 @@ | ||
| 40 | 40 | source = "hashicorp/aws" |
| 41 | 41 | version = "~> 5.0" |
| 42 | 42 | } |
| 43 | awscc = { | |
| 44 | source = "hashicorp/awscc" | |
| 45 | version = "~> 1.0" | |
| 46 | } | |
| 43 | 47 | } |
| 44 | 48 | |
| 45 | 49 | # Fully partial backend — all configuration comes from -backend-config |
| @@ -51,6 +55,10 @@ | ||
| 51 | 55 | region = var.region |
| 52 | 56 | } |
| 53 | 57 | |
| 58 | provider "awscc" { | |
| 59 | region = var.region | |
| 60 | } | |
| 61 | ||
| 54 | 62 | variable "region" { |
| 55 | 63 | type = string |
| 56 | 64 | default = "us-west-2" |
| @@ -69,6 +77,13 @@ | ||
| 69 | 77 | description = "IAM permissions boundary ARN for created roles" |
| 70 | 78 | type = string |
| 71 | 79 | } |
| 80 | ||
| 81 | variable "artifacts_bucket" { | |
| 82 | description = "S3 bucket for build artifacts and agent code" | |
| 83 | type = string | |
| 84 | } | |
| 85 | ||
| 86 | data "aws_caller_identity" "current" {} | |
| 72 | 87 | |
| 73 | 88 | |
| 74 | 89 | # ============================================================================ |
| @@ -222,6 +237,92 @@ | ||
| 222 | 237 | |
| 223 | 238 | |
| 224 | 239 | # ============================================================================ |
| 240 | # AgentCore Runtime — Tonal agent | |
| 241 | # ============================================================================ | |
| 242 | # | |
| 243 | # The Tonal agent is a Python Strands agent deployed to AWS Bedrock AgentCore | |
| 244 | # Runtime. The agent code is packaged as a zip by buildspec.yml and uploaded | |
| 245 | # to S3 by Terraform. AgentCore pulls the zip from S3 and runs it. | |
| 246 | ||
| 247 | # Upload agent code to S3 (Terraform handles the upload, like Lambda's filename) | |
| 248 | resource "aws_s3_object" "agent_code" { | |
| 249 | bucket = var.artifacts_bucket | |
| 250 | key = "agent-code/${var.prefix}-code.zip" | |
| 251 | source = "agent-code.zip" | |
| 252 | etag = filemd5("agent-code.zip") | |
| 253 | } | |
| 254 | ||
| 255 | # Execution role for the agent runtime (what the agent runs as) | |
| 256 | resource "aws_iam_role" "agent_exec" { | |
| 257 | name = "${var.prefix}-agent-exec" | |
| 258 | permissions_boundary = var.boundary_arn | |
| 259 | ||
| 260 | assume_role_policy = jsonencode({ | |
| 261 | Version = "2012-10-17" | |
| 262 | Statement = [{ | |
| 263 | Effect = "Allow" | |
| 264 | Principal = { Service = "bedrock-agentcore.amazonaws.com" } | |
| 265 | Action = "sts:AssumeRole" | |
| 266 | Condition = { | |
| 267 | StringEquals = { "aws:SourceAccount" = data.aws_caller_identity.current.account_id } | |
| 268 | } | |
| 269 | }] | |
| 270 | }) | |
| 271 | } | |
| 272 | ||
| 273 | resource "aws_iam_role_policy" "agent_bedrock" { | |
| 274 | name = "BedrockAccess" | |
| 275 | role = aws_iam_role.agent_exec.id | |
| 276 | policy = jsonencode({ | |
| 277 | Version = "2012-10-17" | |
| 278 | Statement = [ | |
| 279 | { | |
| 280 | Sid = "InvokeModels" | |
| 281 | Effect = "Allow" | |
| 282 | Action = ["bedrock:InvokeModel", "bedrock:InvokeModelWithResponseStream"] | |
| 283 | Resource = "*" | |
| 284 | }, | |
| 285 | { | |
| 286 | Sid = "Logs" | |
| 287 | Effect = "Allow" | |
| 288 | Action = ["logs:CreateLogGroup", "logs:CreateLogStream", "logs:PutLogEvents"] | |
| 289 | Resource = "arn:aws:logs:*:*:*" | |
| 290 | }, | |
| 291 | ] | |
| 292 | }) | |
| 293 | } | |
| 294 | ||
| 295 | # AgentCore Runtime — the agent itself | |
| 296 | # | |
| 297 | # Using the awscc (Cloud Control) provider because the aws provider doesn't | |
| 298 | # have the bedrockagentcore resource yet. The awscc provider maps directly | |
| 299 | # to CloudFormation and is always up-to-date. | |
| 300 | resource "awscc_bedrockagentcore_runtime" "tonal" { | |
| 301 | agent_runtime_name = replace("${var.prefix}-tonal", "-", "_") | |
| 302 | role_arn = aws_iam_role.agent_exec.arn | |
| 303 | description = "Tonal autonomous software contributor" | |
| 304 | ||
| 305 | agent_runtime_artifact = { | |
| 306 | code_configuration = { | |
| 307 | runtime = "PYTHON_3_12" | |
| 308 | entry_point = ["main.py"] | |
| 309 | code = { | |
| 310 | s3 = { | |
| 311 | bucket = aws_s3_object.agent_code.bucket | |
| 312 | prefix = aws_s3_object.agent_code.key | |
| 313 | version_id = aws_s3_object.agent_code.version_id | |
| 314 | } | |
| 315 | } | |
| 316 | } | |
| 317 | } | |
| 318 | ||
| 319 | network_configuration = { | |
| 320 | network_mode = "PUBLIC" | |
| 321 | } | |
| 322 | } | |
| 323 | ||
| 324 | ||
| 325 | # ============================================================================ | |
| 225 | 326 | # Outputs |
| 226 | 327 | # ============================================================================ |
| 227 | 328 | |
| @@ -234,3 +335,8 @@ | ||
| 234 | 335 | description = "Lambda function name" |
| 235 | 336 | value = aws_lambda_function.hello.function_name |
| 236 | 337 | } |
| 338 | ||
| 339 | output "agent_runtime_arn" { | |
| 340 | description = "AgentCore Runtime ARN for the Tonal agent" | |
| 341 | value = awscc_bedrockagentcore_runtime.tonal.agent_runtime_arn | |
| 342 | } | |
| @@ -40,6 +40,10 @@ | |||
| 40 | source = "hashicorp/aws" | 40 | source = "hashicorp/aws" |
| 41 | version = "~> 5.0" | 41 | version = "~> 5.0" |
| 42 | } | 42 | } |
| 43 | awscc = { | ||
| 44 | source = "hashicorp/awscc" | ||
| 45 | version = "~> 1.0" | ||
| 46 | } | ||
| 43 | } | 47 | } |
| 44 | 48 | ||
| 45 | # Fully partial backend — all configuration comes from -backend-config | 49 | # Fully partial backend — all configuration comes from -backend-config |
| @@ -51,6 +55,10 @@ | |||
| 51 | region = var.region | 55 | region = var.region |
| 52 | } | 56 | } |
| 53 | 57 | ||
| 58 | provider "awscc" { | ||
| 59 | region = var.region | ||
| 60 | } | ||
| 61 | |||
| 54 | variable "region" { | 62 | variable "region" { |
| 55 | type = string | 63 | type = string |
| 56 | default = "us-west-2" | 64 | default = "us-west-2" |
| @@ -69,6 +77,13 @@ | |||
| 69 | description = "IAM permissions boundary ARN for created roles" | 77 | description = "IAM permissions boundary ARN for created roles" |
| 70 | type = string | 78 | type = string |
| 71 | } | 79 | } |
| 80 | |||
| 81 | variable "artifacts_bucket" { | ||
| 82 | description = "S3 bucket for build artifacts and agent code" | ||
| 83 | type = string | ||
| 84 | } | ||
| 85 | |||
| 86 | data "aws_caller_identity" "current" {} | ||
| 72 | 87 | ||
| 73 | 88 | ||
| 74 | # ============================================================================ | 89 | # ============================================================================ |
| @@ -222,6 +237,92 @@ | |||
| 222 | 237 | ||
| 223 | 238 | ||
| 224 | # ============================================================================ | 239 | # ============================================================================ |
| 240 | # AgentCore Runtime — Tonal agent | ||
| 241 | # ============================================================================ | ||
| 242 | # | ||
| 243 | # The Tonal agent is a Python Strands agent deployed to AWS Bedrock AgentCore | ||
| 244 | # Runtime. The agent code is packaged as a zip by buildspec.yml and uploaded | ||
| 245 | # to S3 by Terraform. AgentCore pulls the zip from S3 and runs it. | ||
| 246 | |||
| 247 | # Upload agent code to S3 (Terraform handles the upload, like Lambda's filename) | ||
| 248 | resource "aws_s3_object" "agent_code" { | ||
| 249 | bucket = var.artifacts_bucket | ||
| 250 | key = "agent-code/${var.prefix}-code.zip" | ||
| 251 | source = "agent-code.zip" | ||
| 252 | etag = filemd5("agent-code.zip") | ||
| 253 | } | ||
| 254 | |||
| 255 | # Execution role for the agent runtime (what the agent runs as) | ||
| 256 | resource "aws_iam_role" "agent_exec" { | ||
| 257 | name = "${var.prefix}-agent-exec" | ||
| 258 | permissions_boundary = var.boundary_arn | ||
| 259 | |||
| 260 | assume_role_policy = jsonencode({ | ||
| 261 | Version = "2012-10-17" | ||
| 262 | Statement = [{ | ||
| 263 | Effect = "Allow" | ||
| 264 | Principal = { Service = "bedrock-agentcore.amazonaws.com" } | ||
| 265 | Action = "sts:AssumeRole" | ||
| 266 | Condition = { | ||
| 267 | StringEquals = { "aws:SourceAccount" = data.aws_caller_identity.current.account_id } | ||
| 268 | } | ||
| 269 | }] | ||
| 270 | }) | ||
| 271 | } | ||
| 272 | |||
| 273 | resource "aws_iam_role_policy" "agent_bedrock" { | ||
| 274 | name = "BedrockAccess" | ||
| 275 | role = aws_iam_role.agent_exec.id | ||
| 276 | policy = jsonencode({ | ||
| 277 | Version = "2012-10-17" | ||
| 278 | Statement = [ | ||
| 279 | { | ||
| 280 | Sid = "InvokeModels" | ||
| 281 | Effect = "Allow" | ||
| 282 | Action = ["bedrock:InvokeModel", "bedrock:InvokeModelWithResponseStream"] | ||
| 283 | Resource = "*" | ||
| 284 | }, | ||
| 285 | { | ||
| 286 | Sid = "Logs" | ||
| 287 | Effect = "Allow" | ||
| 288 | Action = ["logs:CreateLogGroup", "logs:CreateLogStream", "logs:PutLogEvents"] | ||
| 289 | Resource = "arn:aws:logs:*:*:*" | ||
| 290 | }, | ||
| 291 | ] | ||
| 292 | }) | ||
| 293 | } | ||
| 294 | |||
| 295 | # AgentCore Runtime — the agent itself | ||
| 296 | # | ||
| 297 | # Using the awscc (Cloud Control) provider because the aws provider doesn't | ||
| 298 | # have the bedrockagentcore resource yet. The awscc provider maps directly | ||
| 299 | # to CloudFormation and is always up-to-date. | ||
| 300 | resource "awscc_bedrockagentcore_runtime" "tonal" { | ||
| 301 | agent_runtime_name = replace("${var.prefix}-tonal", "-", "_") | ||
| 302 | role_arn = aws_iam_role.agent_exec.arn | ||
| 303 | description = "Tonal autonomous software contributor" | ||
| 304 | |||
| 305 | agent_runtime_artifact = { | ||
| 306 | code_configuration = { | ||
| 307 | runtime = "PYTHON_3_12" | ||
| 308 | entry_point = ["main.py"] | ||
| 309 | code = { | ||
| 310 | s3 = { | ||
| 311 | bucket = aws_s3_object.agent_code.bucket | ||
| 312 | prefix = aws_s3_object.agent_code.key | ||
| 313 | version_id = aws_s3_object.agent_code.version_id | ||
| 314 | } | ||
| 315 | } | ||
| 316 | } | ||
| 317 | } | ||
| 318 | |||
| 319 | network_configuration = { | ||
| 320 | network_mode = "PUBLIC" | ||
| 321 | } | ||
| 322 | } | ||
| 323 | |||
| 324 | |||
| 325 | # ============================================================================ | ||
| 225 | # Outputs | 326 | # Outputs |
| 226 | # ============================================================================ | 327 | # ============================================================================ |
| 227 | 328 | ||
| @@ -234,3 +335,8 @@ | |||
| 234 | description = "Lambda function name" | 335 | description = "Lambda function name" |
| 235 | value = aws_lambda_function.hello.function_name | 336 | value = aws_lambda_function.hello.function_name |
| 236 | } | 337 | } |
| 338 | |||
| 339 | output "agent_runtime_arn" { | ||
| 340 | description = "AgentCore Runtime ARN for the Tonal agent" | ||
| 341 | value = awscc_bedrockagentcore_runtime.tonal.agent_runtime_arn | ||
| 342 | } | ||