PR #2 bakery-v6

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 }