@aws/agentcore 1.0.0-preview.13 → 1.0.0-preview.15

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (91) hide show
  1. package/README.md +31 -17
  2. package/dist/agent-inspector/index.css +1 -1
  3. package/dist/agent-inspector/index.js +90 -85
  4. package/dist/assets/__tests__/__snapshots__/assets.snapshot.test.ts.snap +966 -106
  5. package/dist/assets/agents/AGENTS.md +14 -0
  6. package/dist/assets/cdk/bin/cdk.ts +42 -20
  7. package/dist/assets/cdk/lib/cdk-stack.ts +19 -14
  8. package/dist/assets/cdk/test/cdk.test.ts +2 -1
  9. package/dist/assets/container/python/Dockerfile +4 -0
  10. package/dist/assets/python/a2a/strands/capabilities/memory/session.py +4 -1
  11. package/dist/assets/python/agui/strands/capabilities/memory/session.py +4 -1
  12. package/dist/assets/python/http/googleadk/base/mcp_client/client.py +7 -7
  13. package/dist/assets/python/http/googleadk/base/pyproject.toml +1 -1
  14. package/dist/assets/python/http/langchain_langgraph/base/mcp_client/client.py +10 -8
  15. package/dist/assets/python/http/langchain_langgraph/base/pyproject.toml +1 -1
  16. package/dist/assets/python/http/openaiagents/base/main.py +19 -9
  17. package/dist/assets/python/http/openaiagents/base/mcp_client/client.py +4 -4
  18. package/dist/assets/python/http/openaiagents/base/pyproject.toml +1 -1
  19. package/dist/assets/python/http/strands/base/main.py +438 -27
  20. package/dist/assets/python/http/strands/base/mcp_client/client.py +45 -5
  21. package/dist/assets/python/http/strands/base/model/load.py +3 -3
  22. package/dist/assets/python/http/strands/base/pyproject.toml +5 -2
  23. package/dist/assets/python/http/strands/base/skills/fetcher.py +279 -0
  24. package/dist/assets/python/http/strands/capabilities/execution-limits/hooks/execution_limits.py +54 -0
  25. package/dist/assets/python/http/strands/capabilities/memory/session.py +4 -1
  26. package/dist/cli/index.mjs +1274 -1088
  27. package/dist/lib/errors/types.d.ts +18 -0
  28. package/dist/lib/errors/types.d.ts.map +1 -1
  29. package/dist/lib/errors/types.js +28 -1
  30. package/dist/lib/errors/types.js.map +1 -1
  31. package/dist/lib/schemas/io/config-io.d.ts.map +1 -1
  32. package/dist/lib/schemas/io/config-io.js +0 -2
  33. package/dist/lib/schemas/io/config-io.js.map +1 -1
  34. package/dist/schema/constants.d.ts +20 -0
  35. package/dist/schema/constants.d.ts.map +1 -1
  36. package/dist/schema/constants.js +25 -1
  37. package/dist/schema/constants.js.map +1 -1
  38. package/dist/schema/schemas/agent-env.d.ts +35 -0
  39. package/dist/schema/schemas/agent-env.d.ts.map +1 -1
  40. package/dist/schema/schemas/agentcore-project.d.ts +184 -25
  41. package/dist/schema/schemas/agentcore-project.d.ts.map +1 -1
  42. package/dist/schema/schemas/agentcore-project.js +81 -31
  43. package/dist/schema/schemas/agentcore-project.js.map +1 -1
  44. package/dist/schema/schemas/auth.d.ts +175 -0
  45. package/dist/schema/schemas/auth.d.ts.map +1 -1
  46. package/dist/schema/schemas/auth.js +126 -3
  47. package/dist/schema/schemas/auth.js.map +1 -1
  48. package/dist/schema/schemas/deployed-state.d.ts +140 -46
  49. package/dist/schema/schemas/deployed-state.d.ts.map +1 -1
  50. package/dist/schema/schemas/deployed-state.js +39 -14
  51. package/dist/schema/schemas/deployed-state.js.map +1 -1
  52. package/dist/schema/schemas/mcp-defs.d.ts +2 -1
  53. package/dist/schema/schemas/mcp-defs.d.ts.map +1 -1
  54. package/dist/schema/schemas/mcp-defs.js +3 -2
  55. package/dist/schema/schemas/mcp-defs.js.map +1 -1
  56. package/dist/schema/schemas/mcp.d.ts +186 -0
  57. package/dist/schema/schemas/mcp.d.ts.map +1 -1
  58. package/dist/schema/schemas/mcp.js +479 -4
  59. package/dist/schema/schemas/mcp.js.map +1 -1
  60. package/dist/schema/schemas/primitives/ab-test.d.ts +0 -6
  61. package/dist/schema/schemas/primitives/ab-test.d.ts.map +1 -1
  62. package/dist/schema/schemas/primitives/ab-test.js +0 -2
  63. package/dist/schema/schemas/primitives/ab-test.js.map +1 -1
  64. package/dist/schema/schemas/primitives/harness.d.ts +209 -11
  65. package/dist/schema/schemas/primitives/harness.d.ts.map +1 -1
  66. package/dist/schema/schemas/primitives/harness.js +268 -21
  67. package/dist/schema/schemas/primitives/harness.js.map +1 -1
  68. package/dist/schema/schemas/primitives/index.d.ts +6 -8
  69. package/dist/schema/schemas/primitives/index.d.ts.map +1 -1
  70. package/dist/schema/schemas/primitives/index.js +6 -5
  71. package/dist/schema/schemas/primitives/index.js.map +1 -1
  72. package/dist/schema/schemas/primitives/knowledge-base.d.ts +104 -0
  73. package/dist/schema/schemas/primitives/knowledge-base.d.ts.map +1 -0
  74. package/dist/schema/schemas/primitives/knowledge-base.js +99 -0
  75. package/dist/schema/schemas/primitives/knowledge-base.js.map +1 -0
  76. package/dist/schema/schemas/primitives/online-eval-config.d.ts +20 -2
  77. package/dist/schema/schemas/primitives/online-eval-config.d.ts.map +1 -1
  78. package/dist/schema/schemas/primitives/online-eval-config.js +43 -5
  79. package/dist/schema/schemas/primitives/online-eval-config.js.map +1 -1
  80. package/dist/schema/schemas/primitives/policy.d.ts +26 -0
  81. package/dist/schema/schemas/primitives/policy.d.ts.map +1 -1
  82. package/dist/schema/schemas/primitives/policy.js +5 -1
  83. package/dist/schema/schemas/primitives/policy.js.map +1 -1
  84. package/npm-shrinkwrap.json +195 -330
  85. package/package.json +10 -8
  86. package/scripts/bundle.mjs +51 -0
  87. package/scripts/run-e2e-local.sh +40 -8
  88. package/dist/schema/schemas/primitives/http-gateway.d.ts +0 -21
  89. package/dist/schema/schemas/primitives/http-gateway.d.ts.map +0 -1
  90. package/dist/schema/schemas/primitives/http-gateway.js +0 -34
  91. package/dist/schema/schemas/primitives/http-gateway.js.map +0 -1
@@ -1,23 +1,75 @@
1
1
  from typing import Any
2
+ {{#if inlineFunctionTools}}
3
+ import json
2
4
 
5
+ from strands.tools.tools import PythonAgentTool
6
+ from strands.types.tools import ToolResult, ToolUse
7
+ {{/if}}
3
8
  from strands import Agent, tool
9
+ {{#if hasSkillsFetcher}}
10
+ from strands import AgentSkills
11
+ {{#if hasFetchedSkills}}
12
+ from skills.fetcher import resolve_s3_skills, resolve_git_skills
13
+ {{/if}}
14
+ {{#if (some gitSkills "credentialArn")}}
15
+ from bedrock_agentcore.services.identity import IdentityClient
16
+ {{/if}}
17
+ {{/if}}
18
+ import asyncio
19
+ {{#if hasShell}}
20
+ import subprocess
21
+ {{/if}}
22
+ {{#if hasFileOperations}}
23
+ import os
24
+ {{/if}}
25
+ {{#if hasExecutionLimits}}
26
+ from strands.tools.executors import SequentialToolExecutor
27
+ from strands.types.exceptions import EventLoopException
28
+ from hooks.execution_limits import ExecutionLimitExceeded, ExecutionLimitsHook
29
+ {{/if}}
4
30
  {{#if hasConfigBundle}}
5
31
  from strands.hooks import HookProvider, HookRegistry, BeforeInvocationEvent, BeforeToolCallEvent
32
+ {{/if}}
33
+ {{#if truncationStrategy}}
34
+ {{#if (eq truncationStrategy "sliding_window")}}
35
+ from strands.agent.conversation_manager.sliding_window_conversation_manager import SlidingWindowConversationManager
36
+ {{/if}}
37
+ {{#if (eq truncationStrategy "summarization")}}
38
+ from strands.agent.conversation_manager.summarizing_conversation_manager import SummarizingConversationManager
39
+ {{/if}}
40
+ {{else}}
41
+ from strands.agent.conversation_manager.null_conversation_manager import NullConversationManager
42
+ {{/if}}
43
+ {{#if hasConfigBundle}}
6
44
  from bedrock_agentcore.runtime.context import BedrockAgentCoreContext
7
45
  {{/if}}
46
+ {{#if hasBrowser}}
47
+ from strands_tools.browser import AgentCoreBrowser
48
+ {{/if}}
49
+ {{#if hasCodeInterpreter}}
50
+ from strands_tools.code_interpreter import AgentCoreCodeInterpreter
51
+ {{/if}}
8
52
  from bedrock_agentcore.runtime import BedrockAgentCoreApp
9
53
  from model.load import load_model
10
54
  {{#if hasGateway}}
11
55
  from mcp_client.client import get_all_gateway_mcp_clients
12
- {{else}}
13
- from mcp_client.client import get_streamable_http_mcp_client
14
56
  {{/if}}
57
+ {{#if remoteMcpTools}}
58
+ from mcp_client.client import get_all_remote_mcp_clients
59
+ {{/if}}
60
+ {{#unless (or hasGateway remoteMcpTools)}}
61
+ {{#unless isExportHarness}}
62
+ from mcp_client.client import get_streamable_http_mcp_client
63
+ {{/unless}}
64
+ {{/unless}}
15
65
  {{#if hasMemory}}
16
66
  from memory.session import get_memory_session_manager
17
67
  {{/if}}
18
- {{#if needsOs}}
68
+ {{#unless hasFileOperations}}
69
+ {{#if (or needsOs (some gitSkills "credentialArn"))}}
19
70
  import os
20
71
  {{/if}}
72
+ {{/unless}}
21
73
  {{#if hasPayment}}
22
74
  from capabilities.payments.payments import create_payments_plugin, PAYMENT_SYSTEM_PROMPT
23
75
  {{/if}}
@@ -25,22 +77,35 @@ from capabilities.payments.payments import create_payments_plugin, PAYMENT_SYSTE
25
77
  app = BedrockAgentCoreApp()
26
78
  log = app.logger
27
79
 
28
- # Define a Streamable HTTP MCP Client
80
+ {{#if (or hasGateway remoteMcpTools)}}
81
+ # Define MCP clients for all configured MCP servers (gateways and/or remote MCP)
82
+ mcp_clients = []
29
83
  {{#if hasGateway}}
30
- mcp_clients = get_all_gateway_mcp_clients()
84
+ mcp_clients += get_all_gateway_mcp_clients()
85
+ {{/if}}
86
+ {{#if remoteMcpTools}}
87
+ mcp_clients += get_all_remote_mcp_clients()
88
+ {{/if}}
31
89
  {{else}}
90
+ {{#unless isExportHarness}}
91
+ # Define a Streamable HTTP MCP Client
32
92
  mcp_clients = [get_streamable_http_mcp_client()]
93
+ {{/unless}}
33
94
  {{/if}}
34
95
 
96
+ {{#if systemPromptText}}
97
+ DEFAULT_SYSTEM_PROMPT = """{{escapePyStr systemPromptText}}"""
98
+ {{else}}
35
99
  DEFAULT_SYSTEM_PROMPT = """
36
100
  You are a helpful assistant. Use tools when appropriate.
37
- {{#if needsOs}}
101
+ {{#if needsOs}}{{#unless isExportHarness}}
38
102
  You have access to the following mounted filesystems. Use file_read, file_write, and list_files with full absolute paths:
39
103
  {{#if sessionStorageMountPath}}- {{sessionStorageMountPath}}: ephemeral session storage (lost when session ends)
40
104
  {{/if}}{{#each efsMounts}}- {{mountPath}}: EFS persistent storage (persists across sessions and agent restarts)
41
105
  {{/each}}{{#each s3Mounts}}- {{mountPath}}: S3 Files persistent storage (durable, backed by S3)
42
- {{/each}}{{/if}}
106
+ {{/each}}{{/unless}}{{/if}}
43
107
  """
108
+ {{/if}}
44
109
 
45
110
  {{#if hasConfigBundle}}
46
111
  DEFAULT_TOOL_DESC = "Return the sum of two numbers"
@@ -49,6 +114,30 @@ DEFAULT_TOOL_DESC = "Return the sum of two numbers"
49
114
  # Define a collection of tools used by the model
50
115
  tools = []
51
116
 
117
+ {{#if inlineFunctionTools}}
118
+ # Inline function tools — stop the agent loop so the tool call streams back to the caller
119
+ def _make_inline_tool(name: str, spec: dict) -> PythonAgentTool:
120
+ def _handler(tool: ToolUse, **kwargs: Any) -> ToolResult:
121
+ kwargs.get("request_state", {})["stop_event_loop"] = True
122
+ return {"toolUseId": tool["toolUseId"], "status": "success", "content": [{"text": " "}]}
123
+ _handler.__name__ = name
124
+ return PythonAgentTool(tool_name=name, tool_spec=spec, tool_func=_handler)
125
+
126
+ {{#each inlineFunctionTools}}
127
+ _INLINE_SPEC_{{snakeCase name}} = {
128
+ "name": "{{name}}",
129
+ "description": {{safeJson description}},
130
+ "inputSchema": {"json": json.loads({{pyJsonStr inputSchema}}) },
131
+ }
132
+ tools.append(_make_inline_tool("{{name}}", _INLINE_SPEC_{{snakeCase name}}))
133
+ {{/each}}
134
+
135
+ _INLINE_FUNCTION_NAMES = { {{#each inlineFunctionTools}}"{{name}}"{{#unless @last}}, {{/unless}}{{/each}} }
136
+
137
+ {{else}}
138
+ _INLINE_FUNCTION_NAMES = set()
139
+
140
+ {{#unless isExportHarness}}
52
141
  # Define a simple function tool
53
142
  {{#if hasConfigBundle}}
54
143
  @tool(description=DEFAULT_TOOL_DESC)
@@ -60,7 +149,116 @@ def add_numbers(a: int, b: int) -> int:
60
149
  return a+b
61
150
  tools.append(add_numbers)
62
151
 
63
- {{#if needsOs}}
152
+ {{/unless}}
153
+ {{/if}}
154
+ {{#if hasBrowser}}
155
+ tools.append(AgentCoreBrowser({{#if browserIdentifier}}identifier="{{browserIdentifier}}"{{/if}}).browser)
156
+ {{/if}}
157
+ {{#if hasCodeInterpreter}}
158
+ tools.append(AgentCoreCodeInterpreter({{#if codeInterpreterIdentifier}}identifier="{{codeInterpreterIdentifier}}"{{/if}}).code_interpreter)
159
+ {{/if}}
160
+ {{#if hasShell}}
161
+ @tool
162
+ def shell(command: str, timeout: int = 300) -> dict:
163
+ """Execute a bash command and return the results.
164
+
165
+ Args:
166
+ command: The bash command to execute
167
+ timeout: Timeout in seconds (default: 300)
168
+
169
+ Returns:
170
+ Dict with stdout, stderr, and exit_code
171
+ """
172
+ result = subprocess.run(
173
+ command, shell=True, capture_output=True, text=True, timeout=timeout
174
+ )
175
+ return {"stdout": result.stdout, "stderr": result.stderr, "exit_code": result.returncode}
176
+
177
+ tools.append(shell)
178
+ {{/if}}
179
+ {{#if hasFileOperations}}
180
+ @tool
181
+ def file_operations(
182
+ command: str,
183
+ path: str,
184
+ old_str: str = None,
185
+ new_str: str = None,
186
+ file_text: str = None,
187
+ insert_line: int = None,
188
+ view_range: list = None,
189
+ ) -> str:
190
+ """Text editor tool for viewing and modifying files.
191
+
192
+ Args:
193
+ command: The command to execute ("view", "str_replace", "create", "insert")
194
+ path: Path to the file or directory
195
+ old_str: Text to replace (for str_replace command)
196
+ new_str: Replacement text (for str_replace and insert commands)
197
+ file_text: Content for new file (for create command)
198
+ insert_line: Line number to insert after (for insert command)
199
+ view_range: [start_line, end_line] for viewing specific lines (for view command)
200
+
201
+ Returns:
202
+ Result of the operation
203
+ """
204
+ try:
205
+ if command == "view":
206
+ if not os.path.exists(path):
207
+ return f"Error: Path '{path}' does not exist"
208
+ if os.path.isdir(path):
209
+ return "\n".join(os.listdir(path))
210
+ with open(path) as f:
211
+ lines = f.read().splitlines()
212
+ if view_range:
213
+ start, end = view_range
214
+ start_idx = max(0, start - 1)
215
+ end_idx = len(lines) if end == -1 else min(len(lines), end)
216
+ lines = lines[start_idx:end_idx]
217
+ start_num = start_idx + 1
218
+ else:
219
+ start_num = 1
220
+ return "\n".join(f"{start_num + i}: {line}" for i, line in enumerate(lines))
221
+ elif command == "str_replace":
222
+ if old_str is None or new_str is None:
223
+ return "Error: str_replace requires both old_str and new_str parameters"
224
+ if not os.path.exists(path):
225
+ return f"Error: File '{path}' does not exist"
226
+ content = open(path).read()
227
+ if old_str not in content:
228
+ return "Error: Text not found in file"
229
+ count = content.count(old_str)
230
+ if count > 1:
231
+ return f"Error: Text appears {count} times in file. Please be more specific."
232
+ open(path, "w").write(content.replace(old_str, new_str, 1))
233
+ return f"Successfully replaced text in '{path}'"
234
+ elif command == "create":
235
+ if file_text is None:
236
+ return "Error: create requires file_text parameter"
237
+ os.makedirs(os.path.dirname(os.path.abspath(path)), exist_ok=True)
238
+ open(path, "w").write(file_text)
239
+ return f"Successfully created file '{path}'"
240
+ elif command == "insert":
241
+ if new_str is None or insert_line is None:
242
+ return "Error: insert requires both new_str and insert_line parameters"
243
+ if not os.path.exists(path):
244
+ return f"Error: File '{path}' does not exist"
245
+ lines = open(path).read().splitlines(True)
246
+ if insert_line == 0:
247
+ lines.insert(0, new_str + "\n")
248
+ elif insert_line >= len(lines):
249
+ lines.append(new_str + "\n")
250
+ else:
251
+ lines.insert(insert_line, new_str + "\n")
252
+ open(path, "w").write("".join(lines))
253
+ return f"Successfully inserted text in '{path}' at line {insert_line + 1}"
254
+ else:
255
+ return f"Error: Unknown command '{command}'"
256
+ except Exception as e:
257
+ return f"Error: {e}"
258
+
259
+ tools.append(file_operations)
260
+ {{/if}}
261
+ {{#if needsOs}}{{#unless isExportHarness}}
64
262
  _MOUNT_PATHS = [
65
263
  {{#if sessionStorageMountPath}}"{{sessionStorageMountPath}}",{{/if}}
66
264
  {{#each efsMounts}}"{{mountPath}}",{{/each}}
@@ -114,12 +312,21 @@ def list_files(path: str) -> str:
114
312
  return f"Error listing '{path}': {e.strerror}"
115
313
 
116
314
  tools.extend([file_read, file_write, list_files])
117
- {{/if}}
315
+ {{/unless}}{{/if}}
118
316
 
317
+ {{#if (or hasGateway remoteMcpTools)}}
318
+ # Add MCP clients to tools
319
+ for mcp_client in mcp_clients:
320
+ if mcp_client:
321
+ tools.append(mcp_client)
322
+ {{else}}
323
+ {{#unless isExportHarness}}
119
324
  # Add MCP client to tools if available
120
325
  for mcp_client in mcp_clients:
121
326
  if mcp_client:
122
327
  tools.append(mcp_client)
328
+ {{/unless}}
329
+ {{/if}}
123
330
 
124
331
  {{#if hasConfigBundle}}
125
332
 
@@ -155,19 +362,62 @@ class ConfigBundleHook(HookProvider):
155
362
 
156
363
  {{/if}}
157
364
 
365
+ def _make_conversation_manager():
366
+ {{#if truncationStrategy}}
367
+ {{#if (eq truncationStrategy "sliding_window")}}
368
+ {{#if truncationConfig}}
369
+ return SlidingWindowConversationManager(**{{safeJson truncationConfig}}, per_turn=True)
370
+ {{else}}
371
+ return SlidingWindowConversationManager(per_turn=True)
372
+ {{/if}}
373
+ {{else}}
374
+ {{#if truncationConfig}}
375
+ return SummarizingConversationManager(**{{safeJson truncationConfig}})
376
+ {{else}}
377
+ return SummarizingConversationManager()
378
+ {{/if}}
379
+ {{/if}}
380
+ {{else}}
381
+ return NullConversationManager()
382
+ {{/if}}
383
+
158
384
  {{#if hasMemory}}
159
385
  {{#unless hasPayment}}
160
386
  def agent_factory():
161
387
  cache = {}
162
- def get_or_create_agent(session_id, user_id):
163
- key = f"{session_id}/{user_id}"
388
+ def get_or_create_agent(session_id, user_id{{#if hasSkillsFetcher}}, skill_plugins=None{{/if}}):
389
+ {{#if actorId}}
390
+ _actor_id = "{{actorId}}"
391
+ {{else}}
392
+ _actor_id = user_id
393
+ {{/if}}
394
+ key = f"{session_id}/{_actor_id}"
164
395
  if key not in cache:
165
396
  cache[key] = Agent(
166
397
  model=load_model(),
167
- session_manager=get_memory_session_manager(session_id, user_id),
398
+ session_manager=get_memory_session_manager(session_id, _actor_id),
399
+ conversation_manager=_make_conversation_manager(),
168
400
  system_prompt=DEFAULT_SYSTEM_PROMPT,
169
- tools=tools{{#if hasConfigBundle}},
170
- hooks=[ConfigBundleHook()]{{/if}}
401
+ tools=tools,
402
+ {{#if hasSkillsFetcher}}
403
+ plugins=skill_plugins or None,
404
+ {{/if}}
405
+ {{#if hasExecutionLimits}}
406
+ tool_executor=SequentialToolExecutor(),
407
+ callback_handler=None,
408
+ {{/if}}
409
+ hooks=[
410
+ {{#if hasExecutionLimits}}
411
+ ExecutionLimitsHook(
412
+ {{#if maxIterations}}max_iterations={{maxIterations}},{{/if}}
413
+ {{#if maxTokens}}max_tokens={{maxTokens}},{{/if}}
414
+ {{#if timeoutSeconds}}timeout_seconds={{timeoutSeconds}},{{/if}}
415
+ ),
416
+ {{/if}}
417
+ {{#if hasConfigBundle}}
418
+ ConfigBundleHook(),
419
+ {{/if}}
420
+ ],
171
421
  )
172
422
  return cache[key]
173
423
  return get_or_create_agent
@@ -175,24 +425,45 @@ get_or_create_agent = agent_factory()
175
425
  {{/unless}}
176
426
  {{else}}
177
427
  {{#if hasConfigBundle}}
178
- def create_agent():
428
+ def create_agent({{#if hasSkillsFetcher}}skill_plugins=None{{/if}}):
179
429
  return Agent(
180
430
  model=load_model(),
181
431
  system_prompt=DEFAULT_SYSTEM_PROMPT,
182
432
  tools=tools,
433
+ conversation_manager=_make_conversation_manager(),
434
+ {{#if hasSkillsFetcher}}
435
+ plugins=skill_plugins or None,
436
+ {{/if}}
183
437
  hooks=[ConfigBundleHook()],
184
438
  )
185
439
  {{else}}
186
440
  {{#unless hasPayment}}
187
441
  _agent = None
188
442
 
189
- def get_or_create_agent():
443
+ def get_or_create_agent({{#if hasSkillsFetcher}}skill_plugins=None{{/if}}):
190
444
  global _agent
191
445
  if _agent is None:
192
446
  _agent = Agent(
193
447
  model=load_model(),
194
448
  system_prompt=DEFAULT_SYSTEM_PROMPT,
195
449
  tools=tools,
450
+ conversation_manager=_make_conversation_manager(),
451
+ {{#if hasSkillsFetcher}}
452
+ plugins=skill_plugins or None,
453
+ {{/if}}
454
+ {{#if hasExecutionLimits}}
455
+ tool_executor=SequentialToolExecutor(),
456
+ callback_handler=None,
457
+ {{/if}}
458
+ hooks=[
459
+ {{#if hasExecutionLimits}}
460
+ ExecutionLimitsHook(
461
+ {{#if maxIterations}}max_iterations={{maxIterations}},{{/if}}
462
+ {{#if maxTokens}}max_tokens={{maxTokens}},{{/if}}
463
+ {{#if timeoutSeconds}}timeout_seconds={{timeoutSeconds}},{{/if}}
464
+ ),
465
+ {{/if}}
466
+ ],
196
467
  )
197
468
  return _agent
198
469
  {{/unless}}
@@ -200,6 +471,42 @@ def get_or_create_agent():
200
471
  {{/if}}
201
472
 
202
473
 
474
+ def _extract_prompt(payload: dict):
475
+ """Accept harness-style messages[], tool_results[], or plain prompt string payloads."""
476
+ if "messages" in payload:
477
+ return payload["messages"]
478
+ if "tool_results" in payload:
479
+ return [{"role": "user", "content": [{"toolResult": {
480
+ "toolUseId": tr["toolUseId"],
481
+ "status": tr.get("status", "success"),
482
+ "content": tr.get("content", []),
483
+ }} for tr in payload["tool_results"]]}]
484
+ return payload.get("prompt", "")
485
+
486
+
487
+ def _has_inline_function_call(messages) -> bool:
488
+ """Return True if messages contains an assistant toolUse for an inline function tool."""
489
+ if not _INLINE_FUNCTION_NAMES or not isinstance(messages, list):
490
+ return False
491
+ for msg in messages:
492
+ if msg.get("role") == "assistant":
493
+ for block in msg.get("content", []):
494
+ if isinstance(block, dict) and block.get("toolUse", {}).get("name") in _INLINE_FUNCTION_NAMES:
495
+ return True
496
+ return False
497
+
498
+
499
+ def _is_inline_function_call(event: dict) -> bool:
500
+ """Check if a contentBlockStart event is for an inline function tool."""
501
+ if not _INLINE_FUNCTION_NAMES:
502
+ return False
503
+ cbs = event.get("contentBlockStart", {})
504
+ start = cbs.get("start", {})
505
+ tool_use = start.get("toolUse") if isinstance(start, dict) else None
506
+ return tool_use is not None and tool_use.get("name") in _INLINE_FUNCTION_NAMES
507
+
508
+
509
+
203
510
  @app.entrypoint
204
511
  async def invoke(payload, context):
205
512
  log.info("Invoking Agent.....")
@@ -211,23 +518,52 @@ async def invoke(payload, context):
211
518
  payments_plugin = create_payments_plugin(user_id, instrument_id, session_id)
212
519
  plugins = [payments_plugin] if payments_plugin else []
213
520
  {{/if}}
521
+ {{#if hasSkillsFetcher}}
522
+ skill_paths = [{{#each pathSkills}}{{safeJson this}}{{#unless @last}}, {{/unless}}{{/each}}]
523
+ {{#if s3Skills}}
524
+ s3_skill_sources = [{{#each s3Skills}}{{safeJson this}}{{#unless @last}}, {{/unless}}{{/each}}]
525
+ skill_paths.extend(await asyncio.to_thread(resolve_s3_skills, s3_skill_sources, None))
526
+ {{/if}}
527
+ {{#if gitSkills}}
528
+ git_skill_sources = [
529
+ {{#each gitSkills}}
530
+ dict(url={{safeJson this.url}}{{#if this.path}}, path={{safeJson this.path}}{{/if}}{{#if this.credentialArn}}, credentialArn={{safeJson this.credentialArn}}{{#if this.username}}, username={{safeJson this.username}}{{/if}}{{/if}}),
531
+ {{/each}}
532
+ ]
533
+ {{#if (some gitSkills "credentialArn")}}
534
+ _git_identity_client = IdentityClient(os.environ.get("AWS_REGION", os.environ.get("AWS_DEFAULT_REGION", "us-east-1")))
535
+ {{else}}
536
+ _git_identity_client = None
537
+ {{/if}}
538
+ skill_paths.extend(await asyncio.to_thread(resolve_git_skills, git_skill_sources, _git_identity_client))
539
+ {{/if}}
540
+ _skill_plugins = [AgentSkills(skills=skill_paths)] if skill_paths else []
541
+ {{/if}}
214
542
 
215
543
  {{#if hasMemory}}
216
544
  {{#if hasPayment}}
217
545
  mem_session_id = getattr(context, 'session_id', 'default-session')
546
+ {{#if actorId}}
547
+ mem_user_id = "{{actorId}}"
548
+ {{else}}
218
549
  mem_user_id = getattr(context, 'user_id', 'default-user')
550
+ {{/if}}
219
551
  agent = Agent(
220
552
  model=load_model(),
221
553
  session_manager=get_memory_session_manager(mem_session_id, mem_user_id),
222
554
  system_prompt=DEFAULT_SYSTEM_PROMPT + PAYMENT_SYSTEM_PROMPT,
223
555
  tools=tools,
224
- plugins=plugins,{{#if hasConfigBundle}}
556
+ plugins=plugins{{#if hasSkillsFetcher}} + _skill_plugins{{/if}},{{#if hasConfigBundle}}
225
557
  hooks=[ConfigBundleHook()],{{/if}}
226
558
  )
227
559
  {{else}}
228
560
  session_id = getattr(context, 'session_id', 'default-session')
561
+ {{#if actorId}}
562
+ user_id = "{{actorId}}"
563
+ {{else}}
229
564
  user_id = getattr(context, 'user_id', 'default-user')
230
- agent = get_or_create_agent(session_id, user_id)
565
+ {{/if}}
566
+ agent = get_or_create_agent(session_id, user_id{{#if hasSkillsFetcher}}, _skill_plugins{{/if}})
231
567
  {{/if}}
232
568
  {{else}}
233
569
  {{#if hasPayment}}
@@ -235,25 +571,100 @@ async def invoke(payload, context):
235
571
  model=load_model(),
236
572
  system_prompt=DEFAULT_SYSTEM_PROMPT + PAYMENT_SYSTEM_PROMPT,
237
573
  tools=tools,
238
- plugins=plugins,{{#if hasConfigBundle}}
574
+ plugins=plugins{{#if hasSkillsFetcher}} + _skill_plugins{{/if}},{{#if hasConfigBundle}}
239
575
  hooks=[ConfigBundleHook()],{{/if}}
240
576
  )
241
577
  {{else}}
242
578
  {{#if hasConfigBundle}}
243
- agent = create_agent()
579
+ agent = create_agent({{#if hasSkillsFetcher}}_skill_plugins{{/if}})
244
580
  {{else}}
245
- agent = get_or_create_agent()
581
+ agent = get_or_create_agent({{#if hasSkillsFetcher}}_skill_plugins{{/if}})
246
582
  {{/if}}
247
583
  {{/if}}
248
584
  {{/if}}
249
585
 
250
- # Execute and format response
251
- stream = agent.stream_async(payload.get("prompt"))
586
+ prompt = _extract_prompt(payload)
587
+
588
+ {{#if inlineFunctionTools}}
589
+ # If Turn 2 carries the harness-style assistant(toolUse)+user(toolResult) pair,
590
+ # strip the placeholder turn Strands stored during Turn 1 so the real toolResult
591
+ # is injected cleanly — same protocol as the harness runtime.
592
+ if _has_inline_function_call(prompt):
593
+ msgs = agent.messages
594
+ if len(msgs) >= 2 and any("toolResult" in b for b in msgs[-1].get("content", [])):
595
+ del msgs[-2:]
596
+ {{/if}}
597
+
598
+ {{#if hasExecutionLimits}}
599
+ timeout_seconds = {{#if timeoutSeconds}}{{timeoutSeconds}}{{else}}None{{/if}}
600
+ timeout_fired = False
601
+ watchdog_task = None
602
+ if timeout_seconds is not None:
603
+ async def _timeout_watchdog():
604
+ nonlocal timeout_fired
605
+ await asyncio.sleep(timeout_seconds)
606
+ timeout_fired = True
607
+ agent.cancel()
608
+ watchdog_task = asyncio.create_task(_timeout_watchdog())
252
609
 
253
- async for event in stream:
254
- # Handle Text parts of the response
255
- if "data" in event and isinstance(event["data"], str):
256
- yield event["data"]
610
+ try:
611
+ {{#if inlineFunctionTools}}
612
+ hit_inline_function = False
613
+ {{/if}}
614
+ async for event in agent.stream_async(
615
+ prompt,
616
+ ):
617
+ if not isinstance(event, dict) or "event" not in event:
618
+ continue
619
+ cbs = event["event"].get("contentBlockStart")
620
+ if cbs is not None and not cbs.get("start"):
621
+ continue
622
+ {{#if inlineFunctionTools}}
623
+ if not hit_inline_function:
624
+ hit_inline_function = _is_inline_function_call(event["event"])
625
+ {{/if}}
626
+ yield event
627
+ {{#if inlineFunctionTools}}
628
+ if hit_inline_function and "messageStop" in event["event"]:
629
+ return
630
+ {{/if}}
631
+
632
+ if timeout_fired:
633
+ yield {"event": {"messageStop": {"stopReason": "timeout_exceeded"}}}
634
+ except EventLoopException as e:
635
+ if isinstance(e.original_exception, ExecutionLimitExceeded):
636
+ yield {"event": {"messageStop": {"stopReason": str(e.original_exception)}}}
637
+ return
638
+ raise
639
+ finally:
640
+ if watchdog_task is not None:
641
+ watchdog_task.cancel()
642
+ try:
643
+ await watchdog_task
644
+ except asyncio.CancelledError:
645
+ pass
646
+ {{else}}
647
+ {{#if inlineFunctionTools}}
648
+ hit_inline_function = False
649
+ {{/if}}
650
+ async for event in agent.stream_async(
651
+ prompt,
652
+ ):
653
+ if not isinstance(event, dict) or "event" not in event:
654
+ continue
655
+ cbs = event["event"].get("contentBlockStart")
656
+ if cbs is not None and not cbs.get("start"):
657
+ continue
658
+ {{#if inlineFunctionTools}}
659
+ if not hit_inline_function:
660
+ hit_inline_function = _is_inline_function_call(event["event"])
661
+ {{/if}}
662
+ yield event
663
+ {{#if inlineFunctionTools}}
664
+ if hit_inline_function and "messageStop" in event["event"]:
665
+ return
666
+ {{/if}}
667
+ {{/if}}
257
668
 
258
669
 
259
670
  if __name__ == "__main__":
@@ -29,18 +29,22 @@ def _get_bearer_token_{{snakeCase name}}(*, access_token: str):
29
29
  {{#each gatewayProviders}}
30
30
  def get_{{snakeCase name}}_mcp_client() -> MCPClient | None:
31
31
  """Returns an MCP Client connected to the {{name}} gateway."""
32
+ {{#if hardcodedUrl}}
33
+ url = {{safeJson hardcodedUrl}}
34
+ {{else}}
32
35
  url = os.environ.get("{{envVarName}}")
33
36
  if not url:
34
37
  logger.warning("{{envVarName}} not set — {{name}} gateway tools unavailable")
35
38
  return None
39
+ {{/if}}
36
40
  {{#if (eq authType "AWS_IAM")}}
37
- return MCPClient(lambda: aws_iam_streamablehttp_client(url, aws_service="bedrock-agentcore", aws_region=os.environ.get("AWS_REGION", os.environ.get("AWS_DEFAULT_REGION"))))
41
+ return MCPClient(lambda: aws_iam_streamablehttp_client(url, aws_service="bedrock-agentcore", aws_region=os.environ.get("AWS_REGION", os.environ.get("AWS_DEFAULT_REGION"))), prefix="{{snakeCase name}}")
38
42
  {{else if (eq authType "CUSTOM_JWT")}}
39
43
  token = _get_bearer_token_{{snakeCase name}}()
40
44
  headers = {"Authorization": f"Bearer {token}"} if token else {}
41
- return MCPClient(lambda: streamablehttp_client(url, headers=headers))
45
+ return MCPClient(lambda: streamablehttp_client(url, headers=headers), prefix="{{snakeCase name}}")
42
46
  {{else}}
43
- return MCPClient(lambda: streamablehttp_client(url))
47
+ return MCPClient(lambda: streamablehttp_client(url), prefix="{{snakeCase name}}")
44
48
  {{/if}}
45
49
 
46
50
  {{/each}}
@@ -53,7 +57,41 @@ def get_all_gateway_mcp_clients() -> list[MCPClient]:
53
57
  clients.append(client)
54
58
  {{/each}}
55
59
  return clients
56
- {{else}}
60
+ {{/if}}
61
+ {{#if remoteMcpTools}}
62
+ {{#if (some remoteMcpTools "headerCredentials")}}
63
+ from bedrock_agentcore.identity.auth import requires_api_key
64
+ {{/if}}
65
+ {{#each remoteMcpTools}}
66
+ {{#if headerCredentials}}
67
+ {{#each headerCredentials}}
68
+ @requires_api_key(provider_name="{{credentialName}}")
69
+ def _get_{{snakeCase ../name}}_{{snakeCase headerKey}}_key(api_key: str) -> str:
70
+ """Fetch {{headerKey}} credential for {{../name}} from AgentCore Identity."""
71
+ return api_key
72
+
73
+ {{/each}}
74
+ {{/if}}
75
+ def get_{{snakeCase name}}_mcp_client() -> MCPClient | None:
76
+ """Returns an MCP Client for the {{name}} remote MCP server."""
77
+ url = {{safeJson url}}
78
+ {{#if headerCredentials}}
79
+ if os.getenv("LOCAL_DEV") == "1":
80
+ headers = { {{#each headerCredentials}}{{safeJson headerKey}}: os.environ.get("{{envVarName}}", ""){{#unless @last}}, {{/unless}}{{/each}} }
81
+ else:
82
+ headers = { {{#each headerCredentials}}{{safeJson headerKey}}: _get_{{snakeCase ../name}}_{{snakeCase headerKey}}_key(){{#unless @last}}, {{/unless}}{{/each}} }
83
+ return MCPClient(lambda: streamablehttp_client(url, headers=headers))
84
+ {{else}}
85
+ return MCPClient(lambda: streamablehttp_client(url))
86
+ {{/if}}
87
+
88
+ {{/each}}
89
+ def get_all_remote_mcp_clients() -> list[MCPClient]:
90
+ """Returns all configured remote MCP clients."""
91
+ clients = [{{#each remoteMcpTools}}get_{{snakeCase name}}_mcp_client(){{#unless @last}}, {{/unless}}{{/each}}]
92
+ return [c for c in clients if c is not None]
93
+ {{/if}}
94
+ {{#unless (or hasGateway remoteMcpTools)}}
57
95
  {{#if isVpc}}
58
96
  # VPC mode: external MCP endpoints are not reachable without a NAT gateway.
59
97
  # Add an AgentCore Gateway with `agentcore add gateway`, or configure your own endpoint below.
@@ -62,6 +100,7 @@ def get_streamable_http_mcp_client() -> MCPClient | None:
62
100
  """No MCP server configured. Add a gateway with `agentcore add gateway`."""
63
101
  return None
64
102
  {{else}}
103
+ {{#unless isExportHarness}}
65
104
  # ExaAI provides information about code through web searches, crawling and code context searches through their platform. Requires no authentication
66
105
  EXAMPLE_MCP_ENDPOINT = "https://mcp.exa.ai/mcp"
67
106
 
@@ -69,5 +108,6 @@ def get_streamable_http_mcp_client() -> MCPClient:
69
108
  """Returns an MCP Client compatible with Strands"""
70
109
  # to use an MCP server that supports bearer authentication, add headers={"Authorization": f"Bearer {access_token}"}
71
110
  return MCPClient(lambda: streamablehttp_client(EXAMPLE_MCP_ENDPOINT))
111
+ {{/unless}}
72
112
  {{/if}}
73
- {{/if}}
113
+ {{/unless}}