@aws/agentcore 1.0.0-preview.3 → 1.0.0-preview.30

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 (330) hide show
  1. package/README.md +193 -33
  2. package/dist/agent-inspector/index.css +1 -1
  3. package/dist/agent-inspector/index.js +99 -84
  4. package/dist/assets/README.md +6 -2
  5. package/dist/assets/__tests__/__snapshots__/assets.snapshot.test.ts.snap +3344 -366
  6. package/dist/assets/__tests__/__snapshots__/dockerfile-render.test.ts.snap +6 -2
  7. package/dist/assets/__tests__/assets.snapshot.test.ts +10 -24
  8. package/dist/assets/__tests__/googleadk-observability.test.ts +23 -0
  9. package/dist/assets/__tests__/googleadk-session-eviction.test.ts +87 -0
  10. package/dist/assets/__tests__/input-validation.test.ts +44 -0
  11. package/dist/assets/__tests__/summarization-namespace.test.ts +43 -0
  12. package/dist/assets/agents/AGENTS.md +91 -14
  13. package/dist/assets/cdk/bin/cdk.ts +109 -24
  14. package/dist/assets/cdk/lib/cdk-stack.ts +227 -19
  15. package/dist/assets/cdk/package.json +9 -9
  16. package/dist/assets/cdk/test/cdk.test.ts +4 -0
  17. package/dist/assets/container/python/Dockerfile +7 -1
  18. package/dist/assets/container/typescript/Dockerfile +25 -0
  19. package/dist/assets/container/typescript/dockerignore.template +24 -0
  20. package/dist/assets/datasets/predefined-v1.jsonl +3 -0
  21. package/dist/assets/datasets/simulated-v1.jsonl +3 -0
  22. package/dist/assets/evaluators/autoevals-lambda/execution-role-policy.json +15 -0
  23. package/dist/assets/evaluators/autoevals-lambda/lambda_function.py +37 -0
  24. package/dist/assets/evaluators/autoevals-lambda/pyproject.toml +22 -0
  25. package/dist/assets/evaluators/deepeval-lambda/execution-role-policy.json +15 -0
  26. package/dist/assets/evaluators/deepeval-lambda/lambda_function.py +29 -0
  27. package/dist/assets/evaluators/deepeval-lambda/pyproject.toml +19 -0
  28. package/dist/assets/mcp/python/pyproject.toml +1 -1
  29. package/dist/assets/python/a2a/googleadk/base/main.py +32 -18
  30. package/dist/assets/python/a2a/googleadk/base/pyproject.toml +6 -4
  31. package/dist/assets/python/a2a/langchain_langgraph/base/main.py +35 -21
  32. package/dist/assets/python/a2a/langchain_langgraph/base/pyproject.toml +4 -3
  33. package/dist/assets/python/a2a/strands/base/main.py +23 -17
  34. package/dist/assets/python/a2a/strands/base/pyproject.toml +3 -3
  35. package/dist/assets/python/a2a/strands/capabilities/memory/session.py +4 -1
  36. package/dist/assets/python/agui/googleadk/base/main.py +62 -2
  37. package/dist/assets/python/agui/googleadk/base/pyproject.toml +6 -5
  38. package/dist/assets/python/agui/langchain_langgraph/base/main.py +56 -0
  39. package/dist/assets/python/agui/langchain_langgraph/base/pyproject.toml +3 -2
  40. package/dist/assets/python/agui/strands/base/main.py +63 -1
  41. package/dist/assets/python/agui/strands/base/pyproject.toml +2 -2
  42. package/dist/assets/python/agui/strands/capabilities/memory/session.py +4 -1
  43. package/dist/assets/python/http/autogen/base/README.md +7 -1
  44. package/dist/assets/python/http/autogen/base/main.py +62 -29
  45. package/dist/assets/python/http/autogen/base/pyproject.toml +1 -1
  46. package/dist/assets/python/http/googleadk/base/README.md +7 -1
  47. package/dist/assets/python/http/googleadk/base/main.py +71 -24
  48. package/dist/assets/python/http/googleadk/base/mcp_client/client.py +7 -7
  49. package/dist/assets/python/http/googleadk/base/pyproject.toml +5 -3
  50. package/dist/assets/python/http/langchain_langgraph/base/README.md +7 -1
  51. package/dist/assets/python/http/langchain_langgraph/base/main.py +119 -24
  52. package/dist/assets/python/http/langchain_langgraph/base/mcp_client/client.py +10 -8
  53. package/dist/assets/python/http/langchain_langgraph/base/pyproject.toml +5 -4
  54. package/dist/assets/python/http/openaiagents/base/README.md +7 -1
  55. package/dist/assets/python/http/openaiagents/base/main.py +63 -32
  56. package/dist/assets/python/http/openaiagents/base/mcp_client/client.py +4 -4
  57. package/dist/assets/python/http/openaiagents/base/pyproject.toml +3 -3
  58. package/dist/assets/python/http/strands/base/README.md +8 -1
  59. package/dist/assets/python/http/strands/base/main.py +607 -47
  60. package/dist/assets/python/http/strands/base/mcp_client/client.py +49 -6
  61. package/dist/assets/python/http/strands/base/model/load.py +119 -3
  62. package/dist/assets/python/http/strands/base/model/mantle_compat.py +21 -0
  63. package/dist/assets/python/http/strands/base/pyproject.toml +11 -5
  64. package/dist/assets/python/http/strands/base/skills/fetcher.py +279 -0
  65. package/dist/assets/python/http/strands/capabilities/execution-limits/hooks/execution_limits.py +54 -0
  66. package/dist/assets/python/http/strands/capabilities/memory/session.py +4 -1
  67. package/dist/assets/python/http/strands/capabilities/payments/__init__.py +4 -0
  68. package/dist/assets/python/http/strands/capabilities/payments/payments.py +142 -0
  69. package/dist/assets/python/mcp/standalone/base/main.py +58 -0
  70. package/dist/assets/python/mcp/standalone/base/pyproject.toml +1 -1
  71. package/dist/assets/typescript/http/strands/base/README.md +42 -0
  72. package/dist/assets/typescript/http/strands/base/gitignore.template +22 -0
  73. package/dist/assets/typescript/http/strands/base/main.ts +149 -0
  74. package/dist/assets/typescript/http/strands/base/mcp_client/client.ts +11 -0
  75. package/dist/assets/typescript/http/strands/base/model/load.ts +102 -0
  76. package/dist/assets/typescript/http/strands/base/package.json +38 -0
  77. package/dist/assets/typescript/http/strands/base/tsconfig.json +19 -0
  78. package/dist/assets/typescript/http/strands/capabilities/memory/memory.ts +52 -0
  79. package/dist/assets/typescript/http/vercelai/base/README.md +42 -0
  80. package/dist/assets/typescript/http/vercelai/base/gitignore.template +22 -0
  81. package/dist/assets/typescript/http/vercelai/base/main.ts +70 -0
  82. package/dist/assets/typescript/http/vercelai/base/model/load.ts +121 -0
  83. package/dist/assets/typescript/http/vercelai/base/package.json +35 -0
  84. package/dist/assets/typescript/http/vercelai/base/tsconfig.json +19 -0
  85. package/dist/cli/index.mjs +1743 -627
  86. package/dist/lib/constants.d.ts +8 -4
  87. package/dist/lib/constants.d.ts.map +1 -1
  88. package/dist/lib/constants.js +13 -7
  89. package/dist/lib/constants.js.map +1 -1
  90. package/dist/lib/dependency-management/index.d.ts +5 -0
  91. package/dist/lib/dependency-management/index.d.ts.map +1 -0
  92. package/dist/lib/dependency-management/index.js +8 -0
  93. package/dist/lib/dependency-management/index.js.map +1 -0
  94. package/dist/lib/dependency-management/messages.d.ts +30 -0
  95. package/dist/lib/dependency-management/messages.d.ts.map +1 -0
  96. package/dist/lib/dependency-management/messages.js +86 -0
  97. package/dist/lib/dependency-management/messages.js.map +1 -0
  98. package/dist/lib/dependency-management/plan.d.ts +25 -0
  99. package/dist/lib/dependency-management/plan.d.ts.map +1 -0
  100. package/dist/lib/dependency-management/plan.js +67 -0
  101. package/dist/lib/dependency-management/plan.js.map +1 -0
  102. package/dist/lib/dependency-management/semver.d.ts +21 -0
  103. package/dist/lib/dependency-management/semver.d.ts.map +1 -0
  104. package/dist/lib/dependency-management/semver.js +51 -0
  105. package/dist/lib/dependency-management/semver.js.map +1 -0
  106. package/dist/lib/dependency-management/sync.d.ts +26 -0
  107. package/dist/lib/dependency-management/sync.d.ts.map +1 -0
  108. package/dist/lib/dependency-management/sync.js +141 -0
  109. package/dist/lib/dependency-management/sync.js.map +1 -0
  110. package/dist/lib/dependency-management/types.d.ts +85 -0
  111. package/dist/lib/dependency-management/types.d.ts.map +1 -0
  112. package/dist/lib/dependency-management/types.js +3 -0
  113. package/dist/lib/dependency-management/types.js.map +1 -0
  114. package/dist/lib/errors/index.d.ts +1 -1
  115. package/dist/lib/errors/index.d.ts.map +1 -1
  116. package/dist/lib/errors/index.js +1 -1
  117. package/dist/lib/errors/index.js.map +1 -1
  118. package/dist/lib/errors/types.d.ts +274 -0
  119. package/dist/lib/errors/types.d.ts.map +1 -0
  120. package/dist/lib/errors/types.js +423 -0
  121. package/dist/lib/errors/types.js.map +1 -0
  122. package/dist/lib/errors/zod.d.ts +8 -0
  123. package/dist/lib/errors/zod.d.ts.map +1 -0
  124. package/dist/lib/errors/{config.js → zod.js} +8 -81
  125. package/dist/lib/errors/zod.js.map +1 -0
  126. package/dist/lib/index.d.ts +3 -0
  127. package/dist/lib/index.d.ts.map +1 -1
  128. package/dist/lib/index.js +4 -1
  129. package/dist/lib/index.js.map +1 -1
  130. package/dist/lib/packaging/build-args.d.ts +6 -0
  131. package/dist/lib/packaging/build-args.d.ts.map +1 -1
  132. package/dist/lib/packaging/build-args.js +11 -2
  133. package/dist/lib/packaging/build-args.js.map +1 -1
  134. package/dist/lib/packaging/build-context-dockerignore.d.ts +23 -0
  135. package/dist/lib/packaging/build-context-dockerignore.d.ts.map +1 -0
  136. package/dist/lib/packaging/build-context-dockerignore.js +63 -0
  137. package/dist/lib/packaging/build-context-dockerignore.js.map +1 -0
  138. package/dist/lib/packaging/build-context.d.ts +21 -0
  139. package/dist/lib/packaging/build-context.d.ts.map +1 -0
  140. package/dist/lib/packaging/build-context.js +21 -0
  141. package/dist/lib/packaging/build-context.js.map +1 -0
  142. package/dist/lib/packaging/container.d.ts.map +1 -1
  143. package/dist/lib/packaging/container.js +20 -9
  144. package/dist/lib/packaging/container.js.map +1 -1
  145. package/dist/lib/packaging/helpers.d.ts +16 -0
  146. package/dist/lib/packaging/helpers.d.ts.map +1 -1
  147. package/dist/lib/packaging/helpers.js +127 -29
  148. package/dist/lib/packaging/helpers.js.map +1 -1
  149. package/dist/lib/packaging/index.d.ts +2 -1
  150. package/dist/lib/packaging/index.d.ts.map +1 -1
  151. package/dist/lib/packaging/index.js +9 -20
  152. package/dist/lib/packaging/index.js.map +1 -1
  153. package/dist/lib/packaging/node.d.ts +2 -2
  154. package/dist/lib/packaging/node.d.ts.map +1 -1
  155. package/dist/lib/packaging/node.js +83 -38
  156. package/dist/lib/packaging/node.js.map +1 -1
  157. package/dist/lib/packaging/python.js +12 -12
  158. package/dist/lib/packaging/python.js.map +1 -1
  159. package/dist/lib/result.d.ts +57 -0
  160. package/dist/lib/result.d.ts.map +1 -0
  161. package/dist/lib/result.js +62 -0
  162. package/dist/lib/result.js.map +1 -0
  163. package/dist/lib/schemas/io/config-io.d.ts +14 -0
  164. package/dist/lib/schemas/io/config-io.d.ts.map +1 -1
  165. package/dist/lib/schemas/io/config-io.js +89 -5
  166. package/dist/lib/schemas/io/config-io.js.map +1 -1
  167. package/dist/lib/schemas/io/global-config.d.ts +41 -0
  168. package/dist/lib/schemas/io/global-config.d.ts.map +1 -0
  169. package/dist/lib/schemas/io/global-config.js +119 -0
  170. package/dist/lib/schemas/io/global-config.js.map +1 -0
  171. package/dist/lib/schemas/io/index.d.ts +1 -2
  172. package/dist/lib/schemas/io/index.d.ts.map +1 -1
  173. package/dist/lib/schemas/io/index.js +1 -4
  174. package/dist/lib/schemas/io/index.js.map +1 -1
  175. package/dist/lib/schemas/io/path-resolver.d.ts +0 -6
  176. package/dist/lib/schemas/io/path-resolver.d.ts.map +1 -1
  177. package/dist/lib/schemas/io/path-resolver.js +3 -13
  178. package/dist/lib/schemas/io/path-resolver.js.map +1 -1
  179. package/dist/lib/secrets/cipher.d.ts +12 -0
  180. package/dist/lib/secrets/cipher.d.ts.map +1 -0
  181. package/dist/lib/secrets/cipher.js +54 -0
  182. package/dist/lib/secrets/cipher.js.map +1 -0
  183. package/dist/lib/secrets/index.d.ts +4 -0
  184. package/dist/lib/secrets/index.d.ts.map +1 -0
  185. package/dist/lib/secrets/index.js +15 -0
  186. package/dist/lib/secrets/index.js.map +1 -0
  187. package/dist/lib/secrets/key-provider.d.ts +16 -0
  188. package/dist/lib/secrets/key-provider.d.ts.map +1 -0
  189. package/dist/lib/secrets/key-provider.js +191 -0
  190. package/dist/lib/secrets/key-provider.js.map +1 -0
  191. package/dist/lib/secrets/sensitive-keys.d.ts +21 -0
  192. package/dist/lib/secrets/sensitive-keys.d.ts.map +1 -0
  193. package/dist/lib/secrets/sensitive-keys.js +67 -0
  194. package/dist/lib/secrets/sensitive-keys.js.map +1 -0
  195. package/dist/lib/time-constants.d.ts +5 -0
  196. package/dist/lib/time-constants.d.ts.map +1 -0
  197. package/dist/lib/time-constants.js +8 -0
  198. package/dist/lib/time-constants.js.map +1 -0
  199. package/dist/lib/utils/env.d.ts +8 -2
  200. package/dist/lib/utils/env.d.ts.map +1 -1
  201. package/dist/lib/utils/env.js +65 -17
  202. package/dist/lib/utils/env.js.map +1 -1
  203. package/dist/lib/utils/index.d.ts +3 -2
  204. package/dist/lib/utils/index.d.ts.map +1 -1
  205. package/dist/lib/utils/index.js +6 -1
  206. package/dist/lib/utils/index.js.map +1 -1
  207. package/dist/lib/utils/polling.d.ts +31 -0
  208. package/dist/lib/utils/polling.d.ts.map +1 -0
  209. package/dist/lib/utils/polling.js +61 -0
  210. package/dist/lib/utils/polling.js.map +1 -0
  211. package/dist/lib/utils/subprocess.d.ts +4 -1
  212. package/dist/lib/utils/subprocess.d.ts.map +1 -1
  213. package/dist/lib/utils/subprocess.js +11 -57
  214. package/dist/lib/utils/subprocess.js.map +1 -1
  215. package/dist/lib/utils/zod.d.ts +16 -0
  216. package/dist/lib/utils/zod.d.ts.map +1 -1
  217. package/dist/lib/utils/zod.js +45 -0
  218. package/dist/lib/utils/zod.js.map +1 -1
  219. package/dist/schema/constants.d.ts +56 -2
  220. package/dist/schema/constants.d.ts.map +1 -1
  221. package/dist/schema/constants.js +82 -7
  222. package/dist/schema/constants.js.map +1 -1
  223. package/dist/schema/schemas/agent-env.d.ts +187 -7
  224. package/dist/schema/schemas/agent-env.d.ts.map +1 -1
  225. package/dist/schema/schemas/agent-env.js +323 -27
  226. package/dist/schema/schemas/agent-env.js.map +1 -1
  227. package/dist/schema/schemas/agentcore-project.d.ts +519 -12
  228. package/dist/schema/schemas/agentcore-project.d.ts.map +1 -1
  229. package/dist/schema/schemas/agentcore-project.js +342 -13
  230. package/dist/schema/schemas/agentcore-project.js.map +1 -1
  231. package/dist/schema/schemas/auth.d.ts +174 -0
  232. package/dist/schema/schemas/auth.d.ts.map +1 -1
  233. package/dist/schema/schemas/auth.js +127 -3
  234. package/dist/schema/schemas/auth.js.map +1 -1
  235. package/dist/schema/schemas/aws-targets.d.ts +18 -0
  236. package/dist/schema/schemas/aws-targets.d.ts.map +1 -1
  237. package/dist/schema/schemas/aws-targets.js +6 -0
  238. package/dist/schema/schemas/aws-targets.js.map +1 -1
  239. package/dist/schema/schemas/connections.d.ts +185 -0
  240. package/dist/schema/schemas/connections.d.ts.map +1 -0
  241. package/dist/schema/schemas/connections.js +176 -0
  242. package/dist/schema/schemas/connections.js.map +1 -0
  243. package/dist/schema/schemas/deployed-state.d.ts +392 -5
  244. package/dist/schema/schemas/deployed-state.d.ts.map +1 -1
  245. package/dist/schema/schemas/deployed-state.js +112 -2
  246. package/dist/schema/schemas/deployed-state.js.map +1 -1
  247. package/dist/schema/schemas/index.d.ts +3 -0
  248. package/dist/schema/schemas/index.d.ts.map +1 -1
  249. package/dist/schema/schemas/index.js +5 -0
  250. package/dist/schema/schemas/index.js.map +1 -1
  251. package/dist/schema/schemas/mcp-defs.d.ts +2 -1
  252. package/dist/schema/schemas/mcp-defs.d.ts.map +1 -1
  253. package/dist/schema/schemas/mcp-defs.js +3 -2
  254. package/dist/schema/schemas/mcp-defs.js.map +1 -1
  255. package/dist/schema/schemas/mcp.d.ts +197 -0
  256. package/dist/schema/schemas/mcp.d.ts.map +1 -1
  257. package/dist/schema/schemas/mcp.js +334 -5
  258. package/dist/schema/schemas/mcp.js.map +1 -1
  259. package/dist/schema/schemas/primitives/ab-test.d.ts +135 -0
  260. package/dist/schema/schemas/primitives/ab-test.d.ts.map +1 -0
  261. package/dist/schema/schemas/primitives/ab-test.js +95 -0
  262. package/dist/schema/schemas/primitives/ab-test.js.map +1 -0
  263. package/dist/schema/schemas/primitives/capacity-provider.d.ts +237 -0
  264. package/dist/schema/schemas/primitives/capacity-provider.d.ts.map +1 -0
  265. package/dist/schema/schemas/primitives/capacity-provider.js +220 -0
  266. package/dist/schema/schemas/primitives/capacity-provider.js.map +1 -0
  267. package/dist/schema/schemas/primitives/config-bundle.d.ts +32 -0
  268. package/dist/schema/schemas/primitives/config-bundle.d.ts.map +1 -0
  269. package/dist/schema/schemas/primitives/config-bundle.js +41 -0
  270. package/dist/schema/schemas/primitives/config-bundle.js.map +1 -0
  271. package/dist/schema/schemas/primitives/dataset.d.ts +48 -0
  272. package/dist/schema/schemas/primitives/dataset.d.ts.map +1 -0
  273. package/dist/schema/schemas/primitives/dataset.js +58 -0
  274. package/dist/schema/schemas/primitives/dataset.js.map +1 -0
  275. package/dist/schema/schemas/primitives/evaluator.d.ts +22 -0
  276. package/dist/schema/schemas/primitives/evaluator.d.ts.map +1 -1
  277. package/dist/schema/schemas/primitives/evaluator.js +28 -3
  278. package/dist/schema/schemas/primitives/evaluator.js.map +1 -1
  279. package/dist/schema/schemas/primitives/harness.d.ts +377 -19
  280. package/dist/schema/schemas/primitives/harness.d.ts.map +1 -1
  281. package/dist/schema/schemas/primitives/harness.js +379 -24
  282. package/dist/schema/schemas/primitives/harness.js.map +1 -1
  283. package/dist/schema/schemas/primitives/index.d.ts +17 -9
  284. package/dist/schema/schemas/primitives/index.d.ts.map +1 -1
  285. package/dist/schema/schemas/primitives/index.js +62 -1
  286. package/dist/schema/schemas/primitives/index.js.map +1 -1
  287. package/dist/schema/schemas/primitives/knowledge-base.d.ts +104 -0
  288. package/dist/schema/schemas/primitives/knowledge-base.d.ts.map +1 -0
  289. package/dist/schema/schemas/primitives/knowledge-base.js +99 -0
  290. package/dist/schema/schemas/primitives/knowledge-base.js.map +1 -0
  291. package/dist/schema/schemas/primitives/memory.d.ts +22 -4
  292. package/dist/schema/schemas/primitives/memory.d.ts.map +1 -1
  293. package/dist/schema/schemas/primitives/memory.js +57 -16
  294. package/dist/schema/schemas/primitives/memory.js.map +1 -1
  295. package/dist/schema/schemas/primitives/online-eval-config.d.ts +21 -2
  296. package/dist/schema/schemas/primitives/online-eval-config.d.ts.map +1 -1
  297. package/dist/schema/schemas/primitives/online-eval-config.js +45 -5
  298. package/dist/schema/schemas/primitives/online-eval-config.js.map +1 -1
  299. package/dist/schema/schemas/primitives/payment.d.ts +86 -0
  300. package/dist/schema/schemas/primitives/payment.d.ts.map +1 -0
  301. package/dist/schema/schemas/primitives/payment.js +88 -0
  302. package/dist/schema/schemas/primitives/payment.js.map +1 -0
  303. package/dist/schema/schemas/primitives/policy.d.ts +26 -0
  304. package/dist/schema/schemas/primitives/policy.d.ts.map +1 -1
  305. package/dist/schema/schemas/primitives/policy.js +5 -1
  306. package/dist/schema/schemas/primitives/policy.js.map +1 -1
  307. package/npm-shrinkwrap.json +16874 -0
  308. package/package.json +49 -21
  309. package/scripts/bump-version.ts +4 -4
  310. package/scripts/bundle.mjs +70 -41
  311. package/scripts/extract-cli-model.mjs +269 -0
  312. package/scripts/extract-cli-model.test.mjs +43 -0
  313. package/scripts/render_adoc.py +350 -0
  314. package/scripts/run-e2e-dev.sh +56 -0
  315. package/scripts/run-e2e-local.sh +145 -0
  316. package/scripts/safe-junit-reporter.ts +7 -0
  317. package/scripts/sanitize-test-artifacts.ts +65 -0
  318. package/scripts/sync-vended-cdk.ts +49 -0
  319. package/scripts/test_render_adoc.py +38 -0
  320. package/dist/lib/errors/config.d.ts +0 -49
  321. package/dist/lib/errors/config.d.ts.map +0 -1
  322. package/dist/lib/errors/config.js.map +0 -1
  323. package/dist/lib/packaging/errors.d.ts +0 -16
  324. package/dist/lib/packaging/errors.d.ts.map +0 -1
  325. package/dist/lib/packaging/errors.js +0 -36
  326. package/dist/lib/packaging/errors.js.map +0 -1
  327. package/dist/lib/schemas/io/cli-config.d.ts +0 -12
  328. package/dist/lib/schemas/io/cli-config.d.ts.map +0 -1
  329. package/dist/lib/schemas/io/cli-config.js +0 -35
  330. package/dist/lib/schemas/io/cli-config.js.map +0 -1
@@ -44,8 +44,8 @@ agentcore status # checks deployment status
44
44
 
45
45
  exports[`Assets Directory Snapshots > CDK assets > cdk/cdk/bin/cdk.ts should match snapshot 1`] = `
46
46
  "#!/usr/bin/env node
47
- import { AgentCoreStack } from '../lib/cdk-stack';
48
- import { ConfigIO, type AwsDeploymentTarget } from '@aws/agentcore-cdk';
47
+ import { AgentCoreStack, type HarnessConfig } from '../lib/cdk-stack';
48
+ import { ConfigIO, HarnessSpecSchema, type AwsDeploymentTarget } from '@aws/agentcore-cdk';
49
49
  import { App, type Environment } from 'aws-cdk-lib';
50
50
  import * as path from 'path';
51
51
  import * as fs from 'fs';
@@ -73,12 +73,14 @@ async function main() {
73
73
  const spec = await configIO.readProjectSpec();
74
74
  const targets = await configIO.readAWSDeploymentTargets();
75
75
 
76
- // Extract MCP configuration from project spec.
77
- // Gateway fields are stored in agentcore.json but may not yet be on the
78
- // AgentCoreProjectSpec type from @aws/agentcore-cdk, so we read them
79
- // dynamically and cast the resulting object.
76
+ // The vended CDK project compiles against the published @aws/agentcore-cdk
77
+ // schema type, which may lag the CLI's own AgentCoreProjectSpec (e.g. payments,
78
+ // harnesses, gateway fields). Cast once so those fields are reachable.
80
79
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
81
80
  const specAny = spec as any;
81
+
82
+ // Extract MCP configuration from project spec.
83
+ // Gateway fields are stored in agentcore.json but may not yet be on the
82
84
  const mcpSpec = specAny.agentCoreGateways?.length
83
85
  ? {
84
86
  agentCoreGateways: specAny.agentCoreGateways,
@@ -99,37 +101,55 @@ async function main() {
99
101
  throw new Error('No deployment targets configured. Please define targets in agentcore/aws-targets.json');
100
102
  }
101
103
 
102
- // Read harness configs for role creation.
103
- // Harness fields may not yet be on the AgentCoreProjectSpec type from @aws/agentcore-cdk,
104
- // so we read them dynamically via specAny (same pattern as gateways above).
105
- // Harness paths in agentcore.json are relative to the project root (parent of agentcore/).
104
+ // Read harness configs: the full validated spec drives the CFN resource; the
105
+ // role-scoped fields drive the IAM role + container build.
106
106
  const projectRoot = path.resolve(configRoot, '..');
107
- const harnessConfigs: {
108
- name: string;
109
- executionRoleArn?: string;
110
- memoryName?: string;
111
- containerUri?: string;
112
- hasDockerfile?: boolean;
113
- dockerfile?: string;
114
- codeLocation?: string;
115
- tools?: { type: string; name: string }[];
116
- apiKeyArn?: string;
117
- }[] = [];
107
+
108
+ // Read non-S3 KB connector-config files and pass their parsed contents to the
109
+ // L3 verbatim. The L3 does not read files; it expects the parsed
110
+ // connectorParameters keyed by the data source's connectorConfigFile path.
111
+ const connectorParametersByFile: Record<string, Record<string, unknown>> = {};
112
+ for (const kb of specAny.knowledgeBases ?? []) {
113
+ for (const ds of kb.dataSources ?? []) {
114
+ if (ds.type !== 'S3' && ds.connectorConfigFile) {
115
+ const abs = path.resolve(projectRoot, ds.connectorConfigFile);
116
+ try {
117
+ connectorParametersByFile[ds.connectorConfigFile] = JSON.parse(fs.readFileSync(abs, 'utf-8'));
118
+ } catch (err) {
119
+ throw new Error(
120
+ \`Could not read connector config '\${ds.connectorConfigFile}' for knowledge base '\${kb.name}' at \${abs}: \${err instanceof Error ? err.message : err}\`
121
+ );
122
+ }
123
+ }
124
+ }
125
+ }
126
+
127
+ // Synthesize an AWS::BedrockAgentCore::Harness resource for each harness entry in the spec.
128
+ const harnessConfigs: HarnessConfig[] = [];
118
129
  for (const entry of specAny.harnesses ?? []) {
119
130
  const harnessDir = path.resolve(projectRoot, entry.path);
120
131
  const harnessPath = path.resolve(harnessDir, 'harness.json');
121
132
  try {
122
- const harnessSpec = JSON.parse(fs.readFileSync(harnessPath, 'utf-8'));
133
+ const harnessSpec = HarnessSpecSchema.parse(JSON.parse(fs.readFileSync(harnessPath, 'utf-8')));
123
134
  harnessConfigs.push({
124
135
  name: entry.name,
125
136
  executionRoleArn: harnessSpec.executionRoleArn,
126
- memoryName: harnessSpec.memory?.name,
137
+ // Only an \`existing\` memory ref carries a name to wire IAM against; managed memory is
138
+ // owned by the harness (no sibling) and disabled has none — both resolve to undefined.
139
+ memoryName: harnessSpec.memory?.mode === 'existing' ? harnessSpec.memory.name : undefined,
127
140
  containerUri: harnessSpec.containerUri,
128
141
  hasDockerfile: !!harnessSpec.dockerfile,
129
142
  dockerfile: harnessSpec.dockerfile,
130
143
  codeLocation: harnessSpec.dockerfile ? harnessDir : undefined,
131
144
  tools: harnessSpec.tools,
145
+ skills: harnessSpec.skills,
132
146
  apiKeyArn: harnessSpec.model?.apiKeyArn,
147
+ efsAccessPoints: harnessSpec.efsAccessPoints,
148
+ s3AccessPoints: harnessSpec.s3AccessPoints,
149
+ apiFormat: harnessSpec.model?.apiFormat,
150
+ // Full spec + dir drive the AWS::BedrockAgentCore::Harness CFN resource.
151
+ spec: harnessSpec,
152
+ harnessDir,
133
153
  });
134
154
  } catch (err) {
135
155
  throw new Error(
@@ -153,11 +173,76 @@ async function main() {
153
173
  | Record<string, { credentialProviderArn: string; clientSecretArn?: string }>
154
174
  | undefined;
155
175
 
176
+ // Payment credential provider ARNs live in the same credentials map as identity credentials
177
+ const paymentCredentials = credentials;
178
+
179
+ const paymentSpec = specAny.payments?.length
180
+ ? specAny.payments.map(
181
+ (p: {
182
+ name: string;
183
+ description?: string;
184
+ authorizerType: 'AWS_IAM' | 'CUSTOM_JWT';
185
+ authorizerConfiguration?: unknown;
186
+ autoPayment?: boolean;
187
+ paymentToolAllowlist?: string[];
188
+ networkPreferences?: string[];
189
+ connectors: {
190
+ name: string;
191
+ provider?: string;
192
+ provisionMode?: 'MANUAL' | 'QUICK_CREATE';
193
+ credentialName?: string;
194
+ }[];
195
+ }) => ({
196
+ name: p.name,
197
+ description: p.description,
198
+ authorizerType: p.authorizerType,
199
+ authorizerConfiguration: p.authorizerConfiguration,
200
+ autoPayment: p.autoPayment,
201
+ paymentToolAllowlist: p.paymentToolAllowlist,
202
+ networkPreferences: p.networkPreferences,
203
+ connectors: p.connectors.map(c => {
204
+ if (c.provisionMode === 'QUICK_CREATE') {
205
+ return {
206
+ name: c.name,
207
+ provider: 'CoinbaseCDP' as const,
208
+ provisionMode: 'QUICK_CREATE' as const,
209
+ };
210
+ }
211
+
212
+ if (!c.credentialName) {
213
+ throw new Error(
214
+ \`Manual payment connector "\${c.name}" on manager "\${p.name}" is missing its credential name.\`
215
+ );
216
+ }
217
+ const credentialProviderArn = paymentCredentials?.[c.credentialName]?.credentialProviderArn;
218
+ if (!credentialProviderArn) {
219
+ // Fail fast with an actionable message rather than passing an empty
220
+ // ARN that fails opaquely server-side at CreatePaymentConnector.
221
+ throw new Error(
222
+ \`Payment connector "\${c.name}" on manager "\${p.name}" references credential \` +
223
+ \`"\${c.credentialName}", but no deployed credential provider was found for it. \` +
224
+ \`Run \\\`agentcore deploy\\\` so the credential provider is created first.\`
225
+ );
226
+ }
227
+ return {
228
+ name: c.name,
229
+ provider: c.provider,
230
+ ...(c.provisionMode && { provisionMode: c.provisionMode }),
231
+ credentialName: c.credentialName,
232
+ credentialProviderArn,
233
+ };
234
+ }),
235
+ })
236
+ )
237
+ : undefined;
238
+
156
239
  new AgentCoreStack(app, stackName, {
157
240
  spec,
158
241
  mcpSpec,
159
242
  credentials,
243
+ connectorParametersByFile,
160
244
  harnesses: harnessConfigs.length > 0 ? harnessConfigs : undefined,
245
+ paymentSpec,
161
246
  env,
162
247
  description: \`AgentCore stack for \${spec.name} deployed to \${target.name} (\${target.region})\`,
163
248
  tags: {
@@ -172,7 +257,7 @@ async function main() {
172
257
 
173
258
  main().catch((error: unknown) => {
174
259
  console.error('AgentCore CDK synthesis failed:', error instanceof Error ? error.message : error);
175
- process.exitCode = 1;
260
+ process.exit(1);
176
261
  });
177
262
  "
178
263
  `;
@@ -299,22 +384,51 @@ exports[`Assets Directory Snapshots > CDK assets > cdk/cdk/lib/cdk-stack.ts shou
299
384
  "import {
300
385
  AgentCoreApplication,
301
386
  AgentCoreMcp,
387
+ AgentCorePaymentManager,
388
+ AgentCorePaymentConnector,
302
389
  type AgentCoreProjectSpec,
303
390
  type AgentCoreMcpSpec,
391
+ type CustomJWTAuthorizerConfig,
392
+ type HarnessDeploymentConfig,
304
393
  } from '@aws/agentcore-cdk';
305
394
  import { CfnOutput, Stack, type StackProps } from 'aws-cdk-lib';
395
+ import * as iam from 'aws-cdk-lib/aws-iam';
306
396
  import { Construct } from 'constructs';
307
397
 
308
- export interface HarnessConfig {
398
+ /**
399
+ * Harness deployment config: role-scoped fields (for IAM role + container build)
400
+ * plus the full validated spec + its config directory so the L3 construct can
401
+ * synthesize the AWS::BedrockAgentCore::Harness resource.
402
+ */
403
+ export type HarnessConfig = HarnessDeploymentConfig;
404
+
405
+ export interface ManualPaymentConnectorSpec {
406
+ name: string;
407
+ provider: 'CoinbaseCDP' | 'StripePrivy';
408
+ provisionMode?: 'MANUAL';
409
+ credentialName: string;
410
+ credentialProviderArn: string;
411
+ }
412
+
413
+ export interface QuickCreatePaymentConnectorSpec {
414
+ name: string;
415
+ provider: 'CoinbaseCDP';
416
+ provisionMode: 'QUICK_CREATE';
417
+ credentialName?: never;
418
+ credentialProviderArn?: never;
419
+ }
420
+
421
+ export type PaymentConnectorSpec = ManualPaymentConnectorSpec | QuickCreatePaymentConnectorSpec;
422
+
423
+ export interface PaymentSpec {
309
424
  name: string;
310
- executionRoleArn?: string;
311
- memoryName?: string;
312
- containerUri?: string;
313
- hasDockerfile?: boolean;
314
- dockerfile?: string;
315
- codeLocation?: string;
316
- tools?: { type: string; name: string }[];
317
- apiKeyArn?: string;
425
+ description?: string;
426
+ authorizerType: 'AWS_IAM' | 'CUSTOM_JWT';
427
+ authorizerConfiguration?: { customJWTAuthorizer: CustomJWTAuthorizerConfig };
428
+ autoPayment?: boolean;
429
+ paymentToolAllowlist?: string[];
430
+ networkPreferences?: string[];
431
+ connectors: PaymentConnectorSpec[];
318
432
  }
319
433
 
320
434
  export interface AgentCoreStackProps extends StackProps {
@@ -331,13 +445,38 @@ export interface AgentCoreStackProps extends StackProps {
331
445
  */
332
446
  credentials?: Record<string, { credentialProviderArn: string; clientSecretArn?: string }>;
333
447
  /**
334
- * Harness role configurations. Each entry creates an IAM execution role for a harness.
335
- *
336
- * When \`hasDockerfile\` is true and \`codeLocation\` is provided (without an explicit
337
- * \`containerUri\`), the L3 construct builds and pushes a container image via CodeBuild
338
- * and emits its URI as a stack output for the post-CDK harness deployer.
448
+ * Harness role configurations.
339
449
  */
340
450
  harnesses?: HarnessConfig[];
451
+ /**
452
+ * Parsed connectorParameters for non-S3 KB data sources, keyed by
453
+ * connectorConfigFile path. Forwarded to AgentCoreApplication.
454
+ */
455
+ connectorParametersByFile?: Record<string, Record<string, unknown>>;
456
+ /**
457
+ * Payment specifications with resolved credential provider ARNs.
458
+ */
459
+ paymentSpec?: PaymentSpec[];
460
+ }
461
+
462
+ function toCdkId(name: string): string {
463
+ return name.replace(/_/g, '');
464
+ }
465
+
466
+ /**
467
+ * Decide whether a deployed runtime should receive payment env vars + IAM grants.
468
+ * Payments today only ships a runtime shim for Python HTTP runtimes; injecting
469
+ * AGENTCORE_PAYMENT_* env vars into TypeScript / MCP / A2A / AGUI runtimes
470
+ * would surface env vars they cannot consume and would dilute least-privilege
471
+ * IAM grants for runtimes that never call ProcessPayment.
472
+ */
473
+ function isPaymentEligibleAgent(agent: { entrypoint?: string; protocol?: string }): boolean {
474
+ if (agent.protocol && agent.protocol !== 'HTTP') {
475
+ return false;
476
+ }
477
+ const entrypoint = typeof agent.entrypoint === 'string' ? agent.entrypoint : '';
478
+ const entrypointFile = entrypoint.split(':')[0] ?? '';
479
+ return entrypointFile.endsWith('.py');
341
480
  }
342
481
 
343
482
  /**
@@ -353,13 +492,21 @@ export class AgentCoreStack extends Stack {
353
492
  constructor(scope: Construct, id: string, props: AgentCoreStackProps) {
354
493
  super(scope, id, props);
355
494
 
356
- const { spec, mcpSpec, credentials, harnesses } = props;
495
+ const { spec, mcpSpec, credentials, harnesses, connectorParametersByFile, paymentSpec } = props;
357
496
 
358
497
  // Create AgentCoreApplication with all agents and harness roles
359
- this.application = new AgentCoreApplication(this, 'Application', {
360
- spec,
361
- harnesses: harnesses?.length ? harnesses : undefined,
362
- });
498
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
499
+ const appProps: Record<string, unknown> = { spec };
500
+ if (harnesses?.length) {
501
+ appProps.harnesses = harnesses;
502
+ }
503
+ if (connectorParametersByFile && Object.keys(connectorParametersByFile).length > 0) {
504
+ appProps.connectorParametersByFile = connectorParametersByFile;
505
+ }
506
+ if (credentials) {
507
+ appProps.credentials = credentials;
508
+ }
509
+ this.application = new AgentCoreApplication(this, 'Application', appProps as any);
363
510
 
364
511
  // Create AgentCoreMcp if there are gateways configured
365
512
  if (mcpSpec?.agentCoreGateways && mcpSpec.agentCoreGateways.length > 0) {
@@ -372,6 +519,152 @@ export class AgentCoreStack extends Stack {
372
519
  });
373
520
  }
374
521
 
522
+ // Create payment infrastructure via CFN constructs
523
+ if (paymentSpec && paymentSpec.length > 0) {
524
+ for (const payment of paymentSpec) {
525
+ const mgrId = toCdkId(payment.name);
526
+ const manager = new AgentCorePaymentManager(this, \`Payment\${mgrId}\`, {
527
+ projectName: spec.name,
528
+ name: payment.name,
529
+ authorizerType: payment.authorizerType,
530
+ description: payment.description,
531
+ authorizerConfiguration: payment.authorizerConfiguration,
532
+ tags: spec.tags,
533
+ });
534
+
535
+ const prefix = \`AGENTCORE_PAYMENT_\${payment.name.toUpperCase().replace(/-/g, '_')}\`;
536
+
537
+ // Wire env vars from construct output tokens into eligible agent environments only.
538
+ // See isPaymentEligibleAgent — non-Python or non-HTTP runtimes have no shim that
539
+ // can consume these env vars, and giving them sts:AssumeRole on the
540
+ // ProcessPaymentRole would broaden the privilege surface unnecessarily.
541
+ for (const env of this.application.environments.values()) {
542
+ if (!isPaymentEligibleAgent(env.agent)) {
543
+ continue;
544
+ }
545
+ env.runtime.addEnvironmentVariable(\`\${prefix}_MANAGER_ARN\`, manager.paymentManagerArn);
546
+ env.runtime.addEnvironmentVariable(\`\${prefix}_PROCESS_PAYMENT_ROLE_ARN\`, manager.processPaymentRoleArn);
547
+
548
+ // Grant runtime execution role permission to assume the ProcessPaymentRole.
549
+ // The ProcessPaymentRole's trust policy allows AccountRootPrincipal, but the
550
+ // caller still needs sts:AssumeRole on its own role to perform the assumption.
551
+ env.runtime.role.addToPrincipalPolicy(
552
+ new iam.PolicyStatement({
553
+ actions: ['sts:AssumeRole'],
554
+ resources: [manager.processPaymentRoleArn],
555
+ })
556
+ );
557
+
558
+ // Grant payment data-plane actions directly to the runtime role.
559
+ //
560
+ // NOTE: This deviates from the canonical role model in the AgentCore Payments
561
+ // beta guide, which assigns Get/List/Create instrument+session actions to a
562
+ // separate ManagementRole and limits the agent's role to ProcessPayment only.
563
+ // The current SDK plugin (AgentCorePaymentsPlugin.generate_payment_header)
564
+ // calls GetPaymentInstrument internally during the 402 auto-pay path, so the
565
+ // runtime role needs read access. CreatePaymentSession is included so
566
+ // \`agentcore invoke --auto-session\` works without a separate ManagementRole
567
+ // call. Tighten this if the SDK is updated to accept pre-fetched instrument
568
+ // details and split create-session into a backend-only flow.
569
+ env.runtime.role.addToPrincipalPolicy(
570
+ new iam.PolicyStatement({
571
+ actions: [
572
+ 'bedrock-agentcore:GetPaymentInstrument',
573
+ 'bedrock-agentcore:ListPaymentInstruments',
574
+ 'bedrock-agentcore:GetPaymentInstrumentBalance',
575
+ 'bedrock-agentcore:GetPaymentSession',
576
+ 'bedrock-agentcore:ListPaymentSessions',
577
+ 'bedrock-agentcore:CreatePaymentSession',
578
+ 'bedrock-agentcore:ProcessPayment',
579
+ ],
580
+ resources: [manager.paymentManagerArn, \`\${manager.paymentManagerArn}/*\`],
581
+ })
582
+ );
583
+
584
+ if (payment.autoPayment !== undefined) {
585
+ env.runtime.addEnvironmentVariable(\`\${prefix}_AUTO_PAYMENT\`, String(payment.autoPayment));
586
+ }
587
+ if (payment.paymentToolAllowlist) {
588
+ env.runtime.addEnvironmentVariable(\`\${prefix}_TOOL_ALLOWLIST\`, payment.paymentToolAllowlist.join(','));
589
+ }
590
+ if (payment.networkPreferences) {
591
+ env.runtime.addEnvironmentVariable(\`\${prefix}_NETWORK_PREFERENCES\`, payment.networkPreferences.join(','));
592
+ }
593
+ if (payment.authorizerType === 'CUSTOM_JWT') {
594
+ env.runtime.addEnvironmentVariable(\`\${prefix}_AUTH_MODE\`, 'bearer');
595
+ }
596
+ }
597
+
598
+ // Create connectors for this manager
599
+ for (const connector of payment.connectors) {
600
+ const connId = toCdkId(connector.name);
601
+ const schemaConnector =
602
+ connector.provisionMode === 'QUICK_CREATE'
603
+ ? connector
604
+ : {
605
+ name: connector.name,
606
+ provider: connector.provider,
607
+ ...(connector.provisionMode && { provisionMode: connector.provisionMode }),
608
+ credentialName: connector.credentialName,
609
+ };
610
+ const compatibilityProps = {
611
+ projectName: spec.name,
612
+ paymentManager: manager,
613
+ connector: schemaConnector,
614
+ // Remove these legacy manual fields after the new L3 release is pinned.
615
+ connectorName: connector.name,
616
+ connectorType: connector.provider,
617
+ ...(connector.provisionMode !== 'QUICK_CREATE' && {
618
+ credentialProviderArn: connector.credentialProviderArn,
619
+ }),
620
+ };
621
+ const conn = new AgentCorePaymentConnector(
622
+ this,
623
+ \`Payment\${mgrId}\${connId}\`,
624
+ compatibilityProps as unknown as ConstructorParameters<typeof AgentCorePaymentConnector>[2]
625
+ );
626
+
627
+ // Wire first connector's ID as env var (eligible agents only)
628
+ if (connector === payment.connectors[0]) {
629
+ for (const env of this.application.environments.values()) {
630
+ if (!isPaymentEligibleAgent(env.agent)) continue;
631
+ env.runtime.addEnvironmentVariable(\`\${prefix}_CONNECTOR_ID\`, conn.paymentConnectorId);
632
+ }
633
+ }
634
+
635
+ new CfnOutput(this, \`Payment\${mgrId}\${connId}ConnectorId\`, {
636
+ value: conn.paymentConnectorId,
637
+ });
638
+ if (connector.provisionMode === 'QUICK_CREATE') {
639
+ const quickCreateConnector = conn as AgentCorePaymentConnector & {
640
+ paymentConnectorStatus: string;
641
+ authorizationUrl: string;
642
+ };
643
+ new CfnOutput(this, \`Payment\${mgrId}\${connId}ConnectorStatus\`, {
644
+ value: quickCreateConnector.paymentConnectorStatus,
645
+ });
646
+ new CfnOutput(this, \`Payment\${mgrId}\${connId}AuthorizationUrl\`, {
647
+ value: quickCreateConnector.authorizationUrl,
648
+ });
649
+ }
650
+ }
651
+
652
+ // CFN Outputs for post-deploy state parsing
653
+ new CfnOutput(this, \`Payment\${mgrId}ManagerArn\`, {
654
+ value: manager.paymentManagerArn,
655
+ });
656
+ new CfnOutput(this, \`Payment\${mgrId}ManagerId\`, {
657
+ value: manager.paymentManagerId,
658
+ });
659
+ new CfnOutput(this, \`Payment\${mgrId}ProcessPaymentRoleArn\`, {
660
+ value: manager.processPaymentRoleArn,
661
+ });
662
+ new CfnOutput(this, \`Payment\${mgrId}ResourceRetrievalRoleArn\`, {
663
+ value: manager.resourceRetrievalRoleArn,
664
+ });
665
+ }
666
+ }
667
+
375
668
  // Stack-level output
376
669
  new CfnOutput(this, 'StackNameOutput', {
377
670
  description: 'Name of the CloudFormation Stack',
@@ -409,18 +702,18 @@ exports[`Assets Directory Snapshots > CDK assets > cdk/cdk/package.json should m
409
702
  "format:check": "prettier --check ."
410
703
  },
411
704
  "devDependencies": {
412
- "@types/jest": "^29.5.14",
413
- "@types/node": "^24.10.1",
414
- "jest": "^29.7.0",
415
- "ts-jest": "^29.2.5",
416
- "aws-cdk": "2.1100.1",
417
- "prettier": "^3.4.2",
705
+ "@types/jest": "~29.5.14",
706
+ "@types/node": "~24.13.3",
707
+ "jest": "~29.7.0",
708
+ "ts-jest": "~29.4.11",
709
+ "aws-cdk": "~2.1126.0",
710
+ "prettier": "~3.9.5",
418
711
  "typescript": "~5.9.3"
419
712
  },
420
713
  "dependencies": {
421
- "@aws/agentcore-cdk": "^0.1.0-alpha.19",
422
- "aws-cdk-lib": "^2.248.0",
423
- "constructs": "^10.0.0"
714
+ "@aws/agentcore-cdk": "0.1.0-alpha.53",
715
+ "aws-cdk-lib": "~2.261.0",
716
+ "constructs": "~10.7.0"
424
717
  }
425
718
  }
426
719
  "
@@ -443,10 +736,14 @@ test('AgentCoreStack synthesizes with empty spec', () => {
443
736
  credentials: [],
444
737
  evaluators: [],
445
738
  onlineEvalConfigs: [],
739
+ configBundles: [],
446
740
  policyEngines: [],
741
+ payments: [],
447
742
  agentCoreGateways: [],
448
743
  mcpRuntimeTools: [],
449
744
  unassignedTargets: [],
745
+ datasets: [],
746
+ knowledgeBases: [],
450
747
  },
451
748
  });
452
749
  const template = Template.fromStack(stack);
@@ -507,6 +804,16 @@ exports[`Assets Directory Snapshots > File listing > should match the expected f
507
804
  "cdk/tsconfig.json",
508
805
  "container/python/Dockerfile",
509
806
  "container/python/dockerignore.template",
807
+ "container/typescript/Dockerfile",
808
+ "container/typescript/dockerignore.template",
809
+ "datasets/predefined-v1.jsonl",
810
+ "datasets/simulated-v1.jsonl",
811
+ "evaluators/autoevals-lambda/execution-role-policy.json",
812
+ "evaluators/autoevals-lambda/lambda_function.py",
813
+ "evaluators/autoevals-lambda/pyproject.toml",
814
+ "evaluators/deepeval-lambda/execution-role-policy.json",
815
+ "evaluators/deepeval-lambda/lambda_function.py",
816
+ "evaluators/deepeval-lambda/pyproject.toml",
510
817
  "evaluators/python-lambda/execution-role-policy.json",
511
818
  "evaluators/python-lambda/lambda_function.py",
512
819
  "evaluators/python-lambda/pyproject.toml",
@@ -596,14 +903,33 @@ exports[`Assets Directory Snapshots > File listing > should match the expected f
596
903
  "python/http/strands/base/mcp_client/client.py",
597
904
  "python/http/strands/base/model/__init__.py",
598
905
  "python/http/strands/base/model/load.py",
906
+ "python/http/strands/base/model/mantle_compat.py",
599
907
  "python/http/strands/base/pyproject.toml",
908
+ "python/http/strands/base/skills/fetcher.py",
909
+ "python/http/strands/capabilities/execution-limits/hooks/execution_limits.py",
600
910
  "python/http/strands/capabilities/memory/__init__.py",
601
911
  "python/http/strands/capabilities/memory/session.py",
912
+ "python/http/strands/capabilities/payments/__init__.py",
913
+ "python/http/strands/capabilities/payments/payments.py",
602
914
  "python/mcp/standalone/base/README.md",
603
915
  "python/mcp/standalone/base/gitignore.template",
604
916
  "python/mcp/standalone/base/main.py",
605
917
  "python/mcp/standalone/base/pyproject.toml",
606
918
  "typescript/.gitkeep",
919
+ "typescript/http/strands/base/README.md",
920
+ "typescript/http/strands/base/gitignore.template",
921
+ "typescript/http/strands/base/main.ts",
922
+ "typescript/http/strands/base/mcp_client/client.ts",
923
+ "typescript/http/strands/base/model/load.ts",
924
+ "typescript/http/strands/base/package.json",
925
+ "typescript/http/strands/base/tsconfig.json",
926
+ "typescript/http/strands/capabilities/memory/memory.ts",
927
+ "typescript/http/vercelai/base/README.md",
928
+ "typescript/http/vercelai/base/gitignore.template",
929
+ "typescript/http/vercelai/base/main.ts",
930
+ "typescript/http/vercelai/base/model/load.ts",
931
+ "typescript/http/vercelai/base/package.json",
932
+ "typescript/http/vercelai/base/tsconfig.json",
607
933
  ]
608
934
  `;
609
935
 
@@ -650,7 +976,7 @@ description = "MCP Server demonstrating HTTP tool patterns"
650
976
  readme = "README.md"
651
977
  requires-python = ">=3.10"
652
978
  dependencies = [
653
- "mcp[cli] >= 1.2.0",
979
+ "mcp[cli] ~= 1.24.0",
654
980
  "httpx >= 0.27.0",
655
981
  "opentelemetry-distro",
656
982
  "opentelemetry-exporter-otlp",
@@ -1050,12 +1376,14 @@ Thumbs.db
1050
1376
  `;
1051
1377
 
1052
1378
  exports[`Assets Directory Snapshots > Python framework assets > python/python/a2a/googleadk/base/main.py should match snapshot 1`] = `
1053
- "import os
1379
+ "{{#if needsOs}}
1380
+ import os
1381
+ {{/if}}
1054
1382
  from google.adk.agents import Agent
1055
1383
  from google.adk.a2a.executor.a2a_agent_executor import A2aAgentExecutor
1056
1384
  from google.adk.runners import Runner
1057
1385
  from google.adk.sessions import InMemorySessionService
1058
- from a2a.types import AgentCapabilities, AgentCard, AgentSkill
1386
+ from a2a.types import AgentCapabilities, AgentCard, AgentInterface, AgentSkill
1059
1387
  from bedrock_agentcore.runtime import serve_a2a
1060
1388
  from model.load import load_model
1061
1389
 
@@ -1069,18 +1397,21 @@ def add_numbers(a: int, b: int) -> int:
1069
1397
 
1070
1398
  tools = [add_numbers]
1071
1399
 
1072
- {{#if sessionStorageMountPath}}
1073
- SESSION_STORAGE_PATH = "{{sessionStorageMountPath}}"
1400
+ {{#if needsOs}}
1401
+ _MOUNT_PATHS = [
1402
+ {{#if sessionStorageMountPath}}"{{sessionStorageMountPath}}",{{/if}}
1403
+ {{#each efsMounts}}"{{mountPath}}",{{/each}}
1404
+ {{#each s3Mounts}}"{{mountPath}}",{{/each}}
1405
+ ]
1074
1406
 
1075
1407
  def _safe_resolve(path: str) -> str:
1076
- """Resolve path safely within the storage boundary."""
1077
- resolved = os.path.realpath(os.path.join(SESSION_STORAGE_PATH, path.lstrip("/")))
1078
- if not resolved.startswith(os.path.realpath(SESSION_STORAGE_PATH)):
1079
- raise ValueError(f"Path '{path}' is outside the storage boundary")
1408
+ resolved = os.path.realpath(path)
1409
+ if not any(resolved == os.path.realpath(m) or resolved.startswith(os.path.realpath(m) + os.sep) for m in _MOUNT_PATHS):
1410
+ raise ValueError(f"Path '{path}' is not within any configured mount ({', '.join(_MOUNT_PATHS)})")
1080
1411
  return resolved
1081
1412
 
1082
1413
  def file_read(path: str) -> str:
1083
- """Read a file from persistent storage. The path is relative to the storage root."""
1414
+ """Read a file from a mounted filesystem. Use the absolute path (e.g. /mnt/tools/data.txt)."""
1084
1415
  try:
1085
1416
  full_path = _safe_resolve(path)
1086
1417
  with open(full_path) as f:
@@ -1091,7 +1422,7 @@ def file_read(path: str) -> str:
1091
1422
  return f"Error reading '{path}': {e.strerror}"
1092
1423
 
1093
1424
  def file_write(path: str, content: str) -> str:
1094
- """Write content to a file in persistent storage. The path is relative to the storage root."""
1425
+ """Write a file to a mounted filesystem. Use the absolute path (e.g. /mnt/tools/data.txt)."""
1095
1426
  try:
1096
1427
  full_path = _safe_resolve(path)
1097
1428
  parent = os.path.dirname(full_path)
@@ -1105,25 +1436,28 @@ def file_write(path: str, content: str) -> str:
1105
1436
  except OSError as e:
1106
1437
  return f"Error writing '{path}': {e.strerror}"
1107
1438
 
1108
- def list_files(directory: str = "") -> str:
1109
- """List files in persistent storage. The directory is relative to the storage root."""
1439
+ def list_files(path: str) -> str:
1440
+ """List files in a mounted filesystem directory. Use the absolute path (e.g. /mnt/tools)."""
1110
1441
  try:
1111
- target = _safe_resolve(directory)
1112
- entries = os.listdir(target)
1442
+ full_path = _safe_resolve(path)
1443
+ entries = os.listdir(full_path)
1113
1444
  return "\\n".join(entries) if entries else "(empty directory)"
1114
1445
  except ValueError as e:
1115
1446
  return str(e)
1116
1447
  except OSError as e:
1117
- return f"Error listing '{directory}': {e.strerror}"
1448
+ return f"Error listing '{path}': {e.strerror}"
1118
1449
 
1119
1450
  tools.extend([file_read, file_write, list_files])
1120
1451
  {{/if}}
1121
1452
 
1122
1453
  AGENT_INSTRUCTION = """
1123
1454
  You are a helpful assistant. Use tools when appropriate.
1124
- {{#if sessionStorageMountPath}}
1125
- You have persistent storage at {{sessionStorageMountPath}}. Use file tools to read and write files. Data persists across sessions.
1126
- {{/if}}
1455
+ {{#if needsOs}}
1456
+ You have access to the following mounted filesystems. Use file_read, file_write, and list_files with full absolute paths:
1457
+ {{#if sessionStorageMountPath}}- {{sessionStorageMountPath}}: ephemeral session storage (lost when session ends)
1458
+ {{/if}}{{#each efsMounts}}- {{mountPath}}: EFS persistent storage (persists across sessions and agent restarts)
1459
+ {{/each}}{{#each s3Mounts}}- {{mountPath}}: S3 Files persistent storage (durable, backed by S3)
1460
+ {{/each}}{{/if}}
1127
1461
  """
1128
1462
 
1129
1463
  agent = Agent(
@@ -1143,7 +1477,6 @@ runner = Runner(
1143
1477
  card = AgentCard(
1144
1478
  name=agent.name,
1145
1479
  description=agent.description,
1146
- url="http://localhost:9000/",
1147
1480
  version="0.1.0",
1148
1481
  capabilities=AgentCapabilities(streaming=True),
1149
1482
  skills=[
@@ -1156,6 +1489,13 @@ card = AgentCard(
1156
1489
  ],
1157
1490
  default_input_modes=["text"],
1158
1491
  default_output_modes=["text"],
1492
+ supported_interfaces=[
1493
+ AgentInterface(
1494
+ protocol_binding="JSONRPC",
1495
+ protocol_version="1.0",
1496
+ url="http://localhost:9000/",
1497
+ )
1498
+ ],
1159
1499
  )
1160
1500
 
1161
1501
  if __name__ == "__main__":
@@ -1225,11 +1565,13 @@ description = "AgentCore A2A Agent using Google ADK"
1225
1565
  readme = "README.md"
1226
1566
  requires-python = ">=3.10"
1227
1567
  dependencies = [
1228
- "a2a-sdk >= 0.2.0",
1568
+ "a2a-sdk[http-server] >= 1.0.1, < 2.0.0",
1229
1569
  "aws-opentelemetry-distro",
1230
- "bedrock-agentcore[a2a] >= 1.0.3",
1231
- "google-adk >= 1.0.0",
1232
- "google-genai >= 1.0.0",
1570
+ "bedrock-agentcore[a2a-v1] >= 1.19.0",
1571
+ "google-adk[a2a] >= 2.5.0, < 3.0.0",
1572
+ "google-genai >= 2.9.0, < 3.0.0",
1573
+ # 1.13.0 was yanked for broken imports.
1574
+ "opentelemetry-resourcedetector-gcp >= 1.9.0a0, < 2.0.0, != 1.13.0",
1233
1575
  ]
1234
1576
 
1235
1577
  [tool.hatch.build.targets.wheel]
@@ -1309,15 +1651,17 @@ Thumbs.db
1309
1651
  `;
1310
1652
 
1311
1653
  exports[`Assets Directory Snapshots > Python framework assets > python/python/a2a/langchain_langgraph/base/main.py should match snapshot 1`] = `
1312
- "import os
1654
+ "{{#if needsOs}}
1655
+ import os
1656
+ {{/if}}
1313
1657
  from langchain_core.tools import tool
1314
1658
  from langgraph.prebuilt import create_react_agent
1315
1659
  from opentelemetry.instrumentation.langchain import LangchainInstrumentor
1660
+ from a2a.helpers import new_task_from_user_message
1316
1661
  from a2a.server.agent_execution import AgentExecutor, RequestContext
1317
1662
  from a2a.server.events import EventQueue
1318
1663
  from a2a.server.tasks import TaskUpdater
1319
- from a2a.types import AgentCapabilities, AgentCard, AgentSkill, Part, TextPart
1320
- from a2a.utils import new_task
1664
+ from a2a.types import AgentCapabilities, AgentCard, AgentInterface, AgentSkill, Part
1321
1665
  from bedrock_agentcore.runtime import serve_a2a
1322
1666
  from model.load import load_model
1323
1667
 
@@ -1332,19 +1676,22 @@ def add_numbers(a: int, b: int) -> int:
1332
1676
 
1333
1677
  tools = [add_numbers]
1334
1678
 
1335
- {{#if sessionStorageMountPath}}
1336
- SESSION_STORAGE_PATH = "{{sessionStorageMountPath}}"
1679
+ {{#if needsOs}}
1680
+ _MOUNT_PATHS = [
1681
+ {{#if sessionStorageMountPath}}"{{sessionStorageMountPath}}",{{/if}}
1682
+ {{#each efsMounts}}"{{mountPath}}",{{/each}}
1683
+ {{#each s3Mounts}}"{{mountPath}}",{{/each}}
1684
+ ]
1337
1685
 
1338
1686
  def _safe_resolve(path: str) -> str:
1339
- """Resolve path safely within the storage boundary."""
1340
- resolved = os.path.realpath(os.path.join(SESSION_STORAGE_PATH, path.lstrip("/")))
1341
- if not resolved.startswith(os.path.realpath(SESSION_STORAGE_PATH)):
1342
- raise ValueError(f"Path '{path}' is outside the storage boundary")
1687
+ resolved = os.path.realpath(path)
1688
+ if not any(resolved == os.path.realpath(m) or resolved.startswith(os.path.realpath(m) + os.sep) for m in _MOUNT_PATHS):
1689
+ raise ValueError(f"Path '{path}' is not within any configured mount ({', '.join(_MOUNT_PATHS)})")
1343
1690
  return resolved
1344
1691
 
1345
1692
  @tool
1346
1693
  def file_read(path: str) -> str:
1347
- """Read a file from persistent storage. The path is relative to the storage root."""
1694
+ """Read a file from a mounted filesystem. Use the absolute path (e.g. /mnt/tools/data.txt)."""
1348
1695
  try:
1349
1696
  full_path = _safe_resolve(path)
1350
1697
  with open(full_path) as f:
@@ -1356,7 +1703,7 @@ def file_read(path: str) -> str:
1356
1703
 
1357
1704
  @tool
1358
1705
  def file_write(path: str, content: str) -> str:
1359
- """Write content to a file in persistent storage. The path is relative to the storage root."""
1706
+ """Write a file to a mounted filesystem. Use the absolute path (e.g. /mnt/tools/data.txt)."""
1360
1707
  try:
1361
1708
  full_path = _safe_resolve(path)
1362
1709
  parent = os.path.dirname(full_path)
@@ -1371,25 +1718,28 @@ def file_write(path: str, content: str) -> str:
1371
1718
  return f"Error writing '{path}': {e.strerror}"
1372
1719
 
1373
1720
  @tool
1374
- def list_files(directory: str = "") -> str:
1375
- """List files in persistent storage. The directory is relative to the storage root."""
1721
+ def list_files(path: str) -> str:
1722
+ """List files in a mounted filesystem directory. Use the absolute path (e.g. /mnt/tools)."""
1376
1723
  try:
1377
- target = _safe_resolve(directory)
1378
- entries = os.listdir(target)
1724
+ full_path = _safe_resolve(path)
1725
+ entries = os.listdir(full_path)
1379
1726
  return "\\n".join(entries) if entries else "(empty directory)"
1380
1727
  except ValueError as e:
1381
1728
  return str(e)
1382
1729
  except OSError as e:
1383
- return f"Error listing '{directory}': {e.strerror}"
1730
+ return f"Error listing '{path}': {e.strerror}"
1384
1731
 
1385
1732
  tools.extend([file_read, file_write, list_files])
1386
1733
  {{/if}}
1387
1734
 
1388
1735
  SYSTEM_PROMPT = """
1389
1736
  You are a helpful assistant. Use tools when appropriate.
1390
- {{#if sessionStorageMountPath}}
1391
- You have persistent storage at {{sessionStorageMountPath}}. Use file tools to read and write files. Data persists across sessions.
1392
- {{/if}}
1737
+ {{#if needsOs}}
1738
+ You have access to the following mounted filesystems. Use file_read, file_write, and list_files with full absolute paths:
1739
+ {{#if sessionStorageMountPath}}- {{sessionStorageMountPath}}: ephemeral session storage (lost when session ends)
1740
+ {{/if}}{{#each efsMounts}}- {{mountPath}}: EFS persistent storage (persists across sessions and agent restarts)
1741
+ {{/each}}{{#each s3Mounts}}- {{mountPath}}: S3 Files persistent storage (durable, backed by S3)
1742
+ {{/each}}{{/if}}
1393
1743
  """
1394
1744
 
1395
1745
  model = load_model()
@@ -1403,7 +1753,7 @@ class LangGraphA2AExecutor(AgentExecutor):
1403
1753
  self.graph = graph
1404
1754
 
1405
1755
  async def execute(self, context: RequestContext, event_queue: EventQueue) -> None:
1406
- task = context.current_task or new_task(context.message)
1756
+ task = context.current_task or new_task_from_user_message(context.message)
1407
1757
  if not context.current_task:
1408
1758
  await event_queue.enqueue_event(task)
1409
1759
  updater = TaskUpdater(event_queue, task.id, task.context_id)
@@ -1412,7 +1762,7 @@ class LangGraphA2AExecutor(AgentExecutor):
1412
1762
  result = await self.graph.ainvoke({"messages": [("user", user_text)]})
1413
1763
  response = result["messages"][-1].content
1414
1764
 
1415
- await updater.add_artifact([Part(root=TextPart(text=response))])
1765
+ await updater.add_artifact([Part(text=response)])
1416
1766
  await updater.complete()
1417
1767
 
1418
1768
  async def cancel(self, context: RequestContext, event_queue: EventQueue) -> None:
@@ -1422,7 +1772,6 @@ class LangGraphA2AExecutor(AgentExecutor):
1422
1772
  card = AgentCard(
1423
1773
  name="{{ name }}",
1424
1774
  description="A LangGraph agent on Bedrock AgentCore",
1425
- url="http://localhost:9000/",
1426
1775
  version="0.1.0",
1427
1776
  capabilities=AgentCapabilities(streaming=True),
1428
1777
  skills=[
@@ -1435,6 +1784,13 @@ card = AgentCard(
1435
1784
  ],
1436
1785
  default_input_modes=["text"],
1437
1786
  default_output_modes=["text"],
1787
+ supported_interfaces=[
1788
+ AgentInterface(
1789
+ protocol_binding="JSONRPC",
1790
+ protocol_version="1.0",
1791
+ url="http://localhost:9000/",
1792
+ )
1793
+ ],
1438
1794
  )
1439
1795
 
1440
1796
  if __name__ == "__main__":
@@ -1586,14 +1942,15 @@ description = "AgentCore A2A Agent using LangChain + LangGraph"
1586
1942
  readme = "README.md"
1587
1943
  requires-python = ">=3.10"
1588
1944
  dependencies = [
1589
- "a2a-sdk >= 0.2.0",
1945
+ "a2a-sdk[http-server] >= 1.0.1, < 2.0.0",
1590
1946
  {{#if (eq modelProvider "Anthropic")}}"langchain-anthropic >= 0.3.0",
1591
1947
  {{/if}}{{#if (eq modelProvider "Bedrock")}}"langchain-aws >= 0.2.0",
1592
1948
  {{/if}}{{#if (eq modelProvider "Gemini")}}"langchain-google-genai >= 2.0.0",
1949
+ "google-genai >= 1.0.0, < 2.0.0",
1593
1950
  {{/if}}{{#if (eq modelProvider "OpenAI")}}"langchain-openai >= 0.2.0",
1594
- {{/if}}"aws-opentelemetry-distro",
1951
+ {{/if}}"aws-opentelemetry-distro ~= 0.18.0",
1595
1952
  "opentelemetry-instrumentation-langchain >= 0.59.0",
1596
- "bedrock-agentcore[a2a] >= 1.0.3",
1953
+ "bedrock-agentcore[a2a-v1] >= 1.19.0",
1597
1954
  "botocore[crt] >= 1.35.0",
1598
1955
  "langgraph >= 0.2.0",
1599
1956
  ]
@@ -1682,7 +2039,7 @@ from model.load import load_model
1682
2039
  {{#if hasMemory}}
1683
2040
  from memory.session import get_memory_session_manager
1684
2041
  {{/if}}
1685
- {{#if sessionStorageMountPath}}
2042
+ {{#if needsOs}}
1686
2043
  import os
1687
2044
  {{/if}}
1688
2045
 
@@ -1695,19 +2052,22 @@ def add_numbers(a: int, b: int) -> int:
1695
2052
 
1696
2053
  tools = [add_numbers]
1697
2054
 
1698
- {{#if sessionStorageMountPath}}
1699
- SESSION_STORAGE_PATH = "{{sessionStorageMountPath}}"
2055
+ {{#if needsOs}}
2056
+ _MOUNT_PATHS = [
2057
+ {{#if sessionStorageMountPath}}"{{sessionStorageMountPath}}",{{/if}}
2058
+ {{#each efsMounts}}"{{mountPath}}",{{/each}}
2059
+ {{#each s3Mounts}}"{{mountPath}}",{{/each}}
2060
+ ]
1700
2061
 
1701
2062
  def _safe_resolve(path: str) -> str:
1702
- """Resolve path safely within the storage boundary."""
1703
- resolved = os.path.realpath(os.path.join(SESSION_STORAGE_PATH, path.lstrip("/")))
1704
- if not resolved.startswith(os.path.realpath(SESSION_STORAGE_PATH)):
1705
- raise ValueError(f"Path '{path}' is outside the storage boundary")
2063
+ resolved = os.path.realpath(path)
2064
+ if not any(resolved == os.path.realpath(m) or resolved.startswith(os.path.realpath(m) + os.sep) for m in _MOUNT_PATHS):
2065
+ raise ValueError(f"Path '{path}' is not within any configured mount ({', '.join(_MOUNT_PATHS)})")
1706
2066
  return resolved
1707
2067
 
1708
2068
  @tool
1709
2069
  def file_read(path: str) -> str:
1710
- """Read a file from persistent storage. The path is relative to the storage root."""
2070
+ """Read a file from a mounted filesystem. Use the absolute path (e.g. /mnt/tools/data.txt)."""
1711
2071
  try:
1712
2072
  full_path = _safe_resolve(path)
1713
2073
  with open(full_path) as f:
@@ -1719,7 +2079,7 @@ def file_read(path: str) -> str:
1719
2079
 
1720
2080
  @tool
1721
2081
  def file_write(path: str, content: str) -> str:
1722
- """Write content to a file in persistent storage. The path is relative to the storage root."""
2082
+ """Write a file to a mounted filesystem. Use the absolute path (e.g. /mnt/tools/data.txt)."""
1723
2083
  try:
1724
2084
  full_path = _safe_resolve(path)
1725
2085
  parent = os.path.dirname(full_path)
@@ -1734,25 +2094,28 @@ def file_write(path: str, content: str) -> str:
1734
2094
  return f"Error writing '{path}': {e.strerror}"
1735
2095
 
1736
2096
  @tool
1737
- def list_files(directory: str = "") -> str:
1738
- """List files in persistent storage. The directory is relative to the storage root."""
2097
+ def list_files(path: str) -> str:
2098
+ """List files in a mounted filesystem directory. Use the absolute path (e.g. /mnt/tools)."""
1739
2099
  try:
1740
- target = _safe_resolve(directory)
1741
- entries = os.listdir(target)
2100
+ full_path = _safe_resolve(path)
2101
+ entries = os.listdir(full_path)
1742
2102
  return "\\n".join(entries) if entries else "(empty directory)"
1743
2103
  except ValueError as e:
1744
2104
  return str(e)
1745
2105
  except OSError as e:
1746
- return f"Error listing '{directory}': {e.strerror}"
2106
+ return f"Error listing '{path}': {e.strerror}"
1747
2107
 
1748
2108
  tools.extend([file_read, file_write, list_files])
1749
2109
  {{/if}}
1750
2110
 
1751
2111
  SYSTEM_PROMPT = """
1752
2112
  You are a helpful assistant. Use tools when appropriate.
1753
- {{#if sessionStorageMountPath}}
1754
- You have persistent storage at {{sessionStorageMountPath}}. Use file tools to read and write files. Data persists across sessions.
1755
- {{/if}}
2113
+ {{#if needsOs}}
2114
+ You have access to the following mounted filesystems. Use file_read, file_write, and list_files with full absolute paths:
2115
+ {{#if sessionStorageMountPath}}- {{sessionStorageMountPath}}: ephemeral session storage (lost when session ends)
2116
+ {{/if}}{{#each efsMounts}}- {{mountPath}}: EFS persistent storage (persists across sessions and agent restarts)
2117
+ {{/each}}{{#each s3Mounts}}- {{mountPath}}: S3 Files persistent storage (durable, backed by S3)
2118
+ {{/each}}{{/if}}
1756
2119
  """
1757
2120
 
1758
2121
  {{#if hasMemory}}
@@ -1930,9 +2293,9 @@ readme = "README.md"
1930
2293
  requires-python = ">=3.10"
1931
2294
  dependencies = [
1932
2295
  {{#if (eq modelProvider "Anthropic")}}"anthropic >= 0.30.0",
1933
- {{/if}}"a2a-sdk[all] >= 0.2.0",
1934
- "aws-opentelemetry-distro",
1935
- "bedrock-agentcore[a2a] >= 1.0.3",
2296
+ {{/if}}"a2a-sdk[all] >= 0.3.0, < 0.4.0",
2297
+ "aws-opentelemetry-distro ~= 0.18.0",
2298
+ "bedrock-agentcore[a2a] >= 1.9.1",
1936
2299
  "botocore[crt] >= 1.35.0",
1937
2300
  {{#if (eq modelProvider "Gemini")}}"google-genai >= 1.0.0",
1938
2301
  {{/if}}{{#if (eq modelProvider "OpenAI")}}"openai >= 1.0.0",
@@ -1976,8 +2339,11 @@ def get_memory_session_manager(session_id: Optional[str], actor_id: str) -> Opti
1976
2339
  {{#if (includes memoryProviders.[0].strategies "USER_PREFERENCE")}}
1977
2340
  f"/users/{actor_id}/preferences": RetrievalConfig(top_k=3, relevance_score=0.5),
1978
2341
  {{/if}}
2342
+ {{#if (includes memoryProviders.[0].strategies "EPISODIC")}}
2343
+ f"/episodes/{actor_id}/{session_id}": RetrievalConfig(top_k=5, relevance_score=0.5),
2344
+ {{/if}}
1979
2345
  {{#if (includes memoryProviders.[0].strategies "SUMMARIZATION")}}
1980
- f"/summaries/{actor_id}/{session_id}": RetrievalConfig(top_k=3, relevance_score=0.5),
2346
+ f"/summaries/{actor_id}": RetrievalConfig(top_k=3, relevance_score=0.5),
1981
2347
  {{/if}}
1982
2348
  }
1983
2349
  {{/if}}
@@ -2079,16 +2445,76 @@ exports[`Assets Directory Snapshots > Python framework assets > python/python/ag
2079
2445
  "import os
2080
2446
  import uvicorn
2081
2447
  from google.adk.agents import LlmAgent
2448
+ from google.adk.tools import FunctionTool
2082
2449
  from ag_ui_adk import ADKAgent, AGUIToolset, create_adk_app
2083
2450
  from model.load import load_model
2084
2451
 
2085
2452
  load_model()
2086
2453
 
2454
+ {{#if needsOs}}
2455
+ _MOUNT_PATHS = [
2456
+ {{#if sessionStorageMountPath}}"{{sessionStorageMountPath}}",{{/if}}
2457
+ {{#each efsMounts}}"{{mountPath}}",{{/each}}
2458
+ {{#each s3Mounts}}"{{mountPath}}",{{/each}}
2459
+ ]
2460
+
2461
+ def _safe_resolve(path: str) -> str:
2462
+ resolved = os.path.realpath(path)
2463
+ if not any(resolved == os.path.realpath(m) or resolved.startswith(os.path.realpath(m) + os.sep) for m in _MOUNT_PATHS):
2464
+ raise ValueError(f"Path '{path}' is not within any configured mount ({', '.join(_MOUNT_PATHS)})")
2465
+ return resolved
2466
+
2467
+ def file_read(path: str) -> str:
2468
+ """Read a file from a mounted filesystem. Use the absolute path (e.g. /mnt/tools/data.txt)."""
2469
+ try:
2470
+ full_path = _safe_resolve(path)
2471
+ with open(full_path) as f:
2472
+ return f.read()
2473
+ except ValueError as e:
2474
+ return str(e)
2475
+ except OSError as e:
2476
+ return f"Error reading '{path}': {e.strerror}"
2477
+
2478
+ def file_write(path: str, content: str) -> str:
2479
+ """Write a file to a mounted filesystem. Use the absolute path (e.g. /mnt/tools/data.txt)."""
2480
+ try:
2481
+ full_path = _safe_resolve(path)
2482
+ parent = os.path.dirname(full_path)
2483
+ if parent:
2484
+ os.makedirs(parent, exist_ok=True)
2485
+ with open(full_path, "w") as f:
2486
+ f.write(content)
2487
+ return f"Written to {path}"
2488
+ except ValueError as e:
2489
+ return str(e)
2490
+ except OSError as e:
2491
+ return f"Error writing '{path}': {e.strerror}"
2492
+
2493
+ def list_files(path: str) -> str:
2494
+ """List files in a mounted filesystem directory. Use the absolute path (e.g. /mnt/tools)."""
2495
+ try:
2496
+ full_path = _safe_resolve(path)
2497
+ entries = os.listdir(full_path)
2498
+ return "\\n".join(entries) if entries else "(empty directory)"
2499
+ except ValueError as e:
2500
+ return str(e)
2501
+ except OSError as e:
2502
+ return f"Error listing '{path}': {e.strerror}"
2503
+
2504
+ _fs_tools = [FunctionTool(file_read), FunctionTool(file_write), FunctionTool(list_files)]
2505
+ {{/if}}
2506
+
2087
2507
  agent = LlmAgent(
2088
2508
  name="{{ name }}",
2089
2509
  model="gemini-2.5-flash",
2090
- instruction="You are a helpful assistant.",
2091
- tools=[AGUIToolset()],
2510
+ instruction="""You are a helpful assistant.
2511
+ {{#if needsOs}}
2512
+ You have access to the following mounted filesystems. Use file_read, file_write, and list_files with full absolute paths:
2513
+ {{#if sessionStorageMountPath}}- {{sessionStorageMountPath}}: ephemeral session storage (lost when session ends)
2514
+ {{/if}}{{#each efsMounts}}- {{mountPath}}: EFS persistent storage (persists across sessions and agent restarts)
2515
+ {{/each}}{{#each s3Mounts}}- {{mountPath}}: S3 Files persistent storage (durable, backed by S3)
2516
+ {{/each}}{{/if}}""",
2517
+ tools=[AGUIToolset(), {{#if needsOs}}*_fs_tools{{/if}}],
2092
2518
  )
2093
2519
 
2094
2520
  adk_agent = ADKAgent(
@@ -2174,12 +2600,13 @@ requires-python = ">=3.10"
2174
2600
  dependencies = [
2175
2601
  "ag-ui-adk >= 0.6.0",
2176
2602
  "ag-ui-protocol >= 0.1.10",
2177
- "bedrock-agentcore >= 1.0.3",
2603
+ "bedrock-agentcore >= 1.8.0",
2178
2604
  "fastapi >= 0.115.12",
2179
- "google-adk >= 1.16.0",
2180
- "google-genai >= 1.0.0",
2181
- "opentelemetry-distro",
2182
- "opentelemetry-exporter-otlp",
2605
+ "google-adk >= 1.16.0, < 2.0.0",
2606
+ "google-genai >= 1.0.0, < 2.0.0",
2607
+ # 1.13.0 was yanked for broken imports.
2608
+ "opentelemetry-resourcedetector-gcp >= 1.9.0a0, < 2.0.0, != 1.13.0",
2609
+ "aws-opentelemetry-distro >= 0.17.0",
2183
2610
  "uvicorn >= 0.34.3",
2184
2611
  ]
2185
2612
 
@@ -2287,6 +2714,62 @@ def add_numbers(a: int, b: int) -> int:
2287
2714
 
2288
2715
 
2289
2716
  backend_tools = [add_numbers]
2717
+
2718
+ {{#if needsOs}}
2719
+ _MOUNT_PATHS = [
2720
+ {{#if sessionStorageMountPath}}"{{sessionStorageMountPath}}",{{/if}}
2721
+ {{#each efsMounts}}"{{mountPath}}",{{/each}}
2722
+ {{#each s3Mounts}}"{{mountPath}}",{{/each}}
2723
+ ]
2724
+
2725
+ def _safe_resolve(path: str) -> str:
2726
+ resolved = os.path.realpath(path)
2727
+ if not any(resolved == os.path.realpath(m) or resolved.startswith(os.path.realpath(m) + os.sep) for m in _MOUNT_PATHS):
2728
+ raise ValueError(f"Path '{path}' is not within any configured mount ({', '.join(_MOUNT_PATHS)})")
2729
+ return resolved
2730
+
2731
+ @tool
2732
+ def file_read(path: str) -> str:
2733
+ """Read a file from a mounted filesystem. Use the absolute path (e.g. /mnt/tools/data.txt)."""
2734
+ try:
2735
+ full_path = _safe_resolve(path)
2736
+ with open(full_path) as f:
2737
+ return f.read()
2738
+ except ValueError as e:
2739
+ return str(e)
2740
+ except OSError as e:
2741
+ return f"Error reading '{path}': {e.strerror}"
2742
+
2743
+ @tool
2744
+ def file_write(path: str, content: str) -> str:
2745
+ """Write a file to a mounted filesystem. Use the absolute path (e.g. /mnt/tools/data.txt)."""
2746
+ try:
2747
+ full_path = _safe_resolve(path)
2748
+ parent = os.path.dirname(full_path)
2749
+ if parent:
2750
+ os.makedirs(parent, exist_ok=True)
2751
+ with open(full_path, "w") as f:
2752
+ f.write(content)
2753
+ return f"Written to {path}"
2754
+ except ValueError as e:
2755
+ return str(e)
2756
+ except OSError as e:
2757
+ return f"Error writing '{path}': {e.strerror}"
2758
+
2759
+ @tool
2760
+ def list_files(path: str) -> str:
2761
+ """List files in a mounted filesystem directory. Use the absolute path (e.g. /mnt/tools)."""
2762
+ try:
2763
+ full_path = _safe_resolve(path)
2764
+ entries = os.listdir(full_path)
2765
+ return "\\n".join(entries) if entries else "(empty directory)"
2766
+ except ValueError as e:
2767
+ return str(e)
2768
+ except OSError as e:
2769
+ return f"Error listing '{path}': {e.strerror}"
2770
+
2771
+ backend_tools.extend([file_read, file_write, list_files])
2772
+ {{/if}}
2290
2773
  model = load_model()
2291
2774
 
2292
2775
 
@@ -2486,10 +2969,11 @@ dependencies = [
2486
2969
  {{#if (eq modelProvider "Anthropic")}}"langchain-anthropic >= 0.3.0",
2487
2970
  {{/if}}{{#if (eq modelProvider "Bedrock")}}"langchain-aws >= 0.2.0",
2488
2971
  {{/if}}{{#if (eq modelProvider "Gemini")}}"langchain-google-genai >= 2.0.0",
2972
+ "google-genai >= 1.0.0, < 2.0.0",
2489
2973
  {{/if}}{{#if (eq modelProvider "OpenAI")}}"langchain-openai >= 0.2.0",
2490
- {{/if}}"aws-opentelemetry-distro",
2974
+ {{/if}}"aws-opentelemetry-distro ~= 0.18.0",
2491
2975
  "opentelemetry-instrumentation-langchain >= 0.59.0",
2492
- "bedrock-agentcore >= 1.0.3",
2976
+ "bedrock-agentcore >= 1.8.0",
2493
2977
  "botocore[crt] >= 1.35.0",
2494
2978
  "langgraph >= 0.3.25",
2495
2979
  "langchain >= 0.3.0",
@@ -2598,9 +3082,71 @@ def add_numbers(a: int, b: int) -> int:
2598
3082
 
2599
3083
  tools = [add_numbers]
2600
3084
 
3085
+ {{#if needsOs}}
3086
+ _MOUNT_PATHS = [
3087
+ {{#if sessionStorageMountPath}}"{{sessionStorageMountPath}}",{{/if}}
3088
+ {{#each efsMounts}}"{{mountPath}}",{{/each}}
3089
+ {{#each s3Mounts}}"{{mountPath}}",{{/each}}
3090
+ ]
3091
+
3092
+ def _safe_resolve(path: str) -> str:
3093
+ resolved = os.path.realpath(path)
3094
+ if not any(resolved == os.path.realpath(m) or resolved.startswith(os.path.realpath(m) + os.sep) for m in _MOUNT_PATHS):
3095
+ raise ValueError(f"Path '{path}' is not within any configured mount ({', '.join(_MOUNT_PATHS)})")
3096
+ return resolved
3097
+
3098
+ @tool
3099
+ def file_read(path: str) -> str:
3100
+ """Read a file from a mounted filesystem. Use the absolute path (e.g. /mnt/tools/data.txt)."""
3101
+ try:
3102
+ full_path = _safe_resolve(path)
3103
+ with open(full_path) as f:
3104
+ return f.read()
3105
+ except ValueError as e:
3106
+ return str(e)
3107
+ except OSError as e:
3108
+ return f"Error reading '{path}': {e.strerror}"
3109
+
3110
+ @tool
3111
+ def file_write(path: str, content: str) -> str:
3112
+ """Write a file to a mounted filesystem. Use the absolute path (e.g. /mnt/tools/data.txt)."""
3113
+ try:
3114
+ full_path = _safe_resolve(path)
3115
+ parent = os.path.dirname(full_path)
3116
+ if parent:
3117
+ os.makedirs(parent, exist_ok=True)
3118
+ with open(full_path, "w") as f:
3119
+ f.write(content)
3120
+ return f"Written to {path}"
3121
+ except ValueError as e:
3122
+ return str(e)
3123
+ except OSError as e:
3124
+ return f"Error writing '{path}': {e.strerror}"
3125
+
3126
+ @tool
3127
+ def list_files(path: str) -> str:
3128
+ """List files in a mounted filesystem directory. Use the absolute path (e.g. /mnt/tools)."""
3129
+ try:
3130
+ full_path = _safe_resolve(path)
3131
+ entries = os.listdir(full_path)
3132
+ return "\\n".join(entries) if entries else "(empty directory)"
3133
+ except ValueError as e:
3134
+ return str(e)
3135
+ except OSError as e:
3136
+ return f"Error listing '{path}': {e.strerror}"
3137
+
3138
+ tools.extend([file_read, file_write, list_files])
3139
+ {{/if}}
3140
+
2601
3141
  agent = Agent(
2602
3142
  model=load_model(),
2603
- system_prompt="You are a helpful assistant. Use tools when appropriate.",
3143
+ system_prompt="""You are a helpful assistant. Use tools when appropriate.
3144
+ {{#if needsOs}}
3145
+ You have access to the following mounted filesystems. Use file_read, file_write, and list_files with full absolute paths:
3146
+ {{#if sessionStorageMountPath}}- {{sessionStorageMountPath}}: ephemeral session storage (lost when session ends)
3147
+ {{/if}}{{#each efsMounts}}- {{mountPath}}: EFS persistent storage (persists across sessions and agent restarts)
3148
+ {{/each}}{{#each s3Mounts}}- {{mountPath}}: S3 Files persistent storage (durable, backed by S3)
3149
+ {{/each}}{{/if}}""",
2604
3150
  tools=tools,
2605
3151
  )
2606
3152
 
@@ -2768,8 +3314,8 @@ dependencies = [
2768
3314
  {{#if (eq modelProvider "Anthropic")}}"anthropic >= 0.30.0",
2769
3315
  {{/if}}"ag-ui-strands >= 0.1.7",
2770
3316
  "ag-ui-protocol >= 0.1.10",
2771
- "aws-opentelemetry-distro",
2772
- "bedrock-agentcore >= 1.0.3",
3317
+ "aws-opentelemetry-distro ~= 0.18.0",
3318
+ "bedrock-agentcore >= 1.9.1",
2773
3319
  "botocore[crt] >= 1.35.0",
2774
3320
  "fastapi >= 0.115.12",
2775
3321
  {{#if (eq modelProvider "Gemini")}}"google-genai >= 1.0.0",
@@ -2816,8 +3362,11 @@ def get_memory_session_manager(session_id: Optional[str], actor_id: str) -> Opti
2816
3362
  {{#if (includes memoryProviders.[0].strategies "USER_PREFERENCE")}}
2817
3363
  f"/users/{actor_id}/preferences": RetrievalConfig(top_k=3, relevance_score=0.5),
2818
3364
  {{/if}}
3365
+ {{#if (includes memoryProviders.[0].strategies "EPISODIC")}}
3366
+ f"/episodes/{actor_id}/{session_id}": RetrievalConfig(top_k=5, relevance_score=0.5),
3367
+ {{/if}}
2819
3368
  {{#if (includes memoryProviders.[0].strategies "SUMMARIZATION")}}
2820
- f"/summaries/{actor_id}/{session_id}": RetrievalConfig(top_k=3, relevance_score=0.5),
3369
+ f"/summaries/{actor_id}": RetrievalConfig(top_k=3, relevance_score=0.5),
2821
3370
  {{/if}}
2822
3371
  }
2823
3372
  {{/if}}
@@ -2852,6 +3401,11 @@ file defines a Starlette ASGI app with the AutoGen framework running within.
2852
3401
 
2853
3402
  \`model/load.py\` instantiates your chosen model provider.
2854
3403
 
3404
+ ## Input Validation
3405
+
3406
+ Validate invocation input before forwarding it to the agent framework. Keep plain prompts typed as strings and pass
3407
+ only prompt text to the agent.
3408
+
2855
3409
  ## Environment Variables
2856
3410
 
2857
3411
  | Variable | Required | Description |
@@ -2863,7 +3417,8 @@ file defines a Starlette ASGI app with the AutoGen framework running within.
2863
3417
 
2864
3418
  If installation was successful, a virtual environment is already created with dependencies installed.
2865
3419
 
2866
- Run \`source .venv/bin/activate\` before developing.
3420
+ Activate the environment with \`source .venv/bin/activate\` on macOS/Linux, \`.venv\\Scripts\\activate.bat\` in Windows
3421
+ Command Prompt, or \`.\\.venv\\Scripts\\activate.ps1\` in Windows PowerShell.
2867
3422
 
2868
3423
  \`agentcore dev\` will start a local server on 0.0.0.0:8080.
2869
3424
 
@@ -2925,7 +3480,10 @@ Thumbs.db
2925
3480
  `;
2926
3481
 
2927
3482
  exports[`Assets Directory Snapshots > Python framework assets > python/python/http/autogen/base/main.py should match snapshot 1`] = `
2928
- "import os
3483
+ "{{#if needsOs}}
3484
+ import os
3485
+ {{/if}}
3486
+ from collections import OrderedDict
2929
3487
  from autogen_agentchat.agents import AssistantAgent
2930
3488
  from autogen_core.tools import FunctionTool
2931
3489
  from bedrock_agentcore.runtime import BedrockAgentCoreApp
@@ -2949,18 +3507,21 @@ add_numbers_tool = FunctionTool(
2949
3507
  # Define a collection of tools used by the model
2950
3508
  tools = [add_numbers_tool]
2951
3509
 
2952
- {{#if sessionStorageMountPath}}
2953
- SESSION_STORAGE_PATH = "{{sessionStorageMountPath}}"
3510
+ {{#if needsOs}}
3511
+ _MOUNT_PATHS = [
3512
+ {{#if sessionStorageMountPath}}"{{sessionStorageMountPath}}",{{/if}}
3513
+ {{#each efsMounts}}"{{mountPath}}",{{/each}}
3514
+ {{#each s3Mounts}}"{{mountPath}}",{{/each}}
3515
+ ]
2954
3516
 
2955
3517
  def _safe_resolve(path: str) -> str:
2956
- """Resolve path safely within the storage boundary."""
2957
- resolved = os.path.realpath(os.path.join(SESSION_STORAGE_PATH, path.lstrip("/")))
2958
- if not resolved.startswith(os.path.realpath(SESSION_STORAGE_PATH)):
2959
- raise ValueError(f"Path '{path}' is outside the storage boundary")
3518
+ resolved = os.path.realpath(path)
3519
+ if not any(resolved == os.path.realpath(m) or resolved.startswith(os.path.realpath(m) + os.sep) for m in _MOUNT_PATHS):
3520
+ raise ValueError(f"Path '{path}' is not within any configured mount ({', '.join(_MOUNT_PATHS)})")
2960
3521
  return resolved
2961
3522
 
2962
3523
  def file_read(path: str) -> str:
2963
- """Read a file from persistent storage. The path is relative to the storage root."""
3524
+ """Read a file from a mounted filesystem. Use the absolute path (e.g. /mnt/tools/data.txt)."""
2964
3525
  try:
2965
3526
  full_path = _safe_resolve(path)
2966
3527
  with open(full_path) as f:
@@ -2971,7 +3532,7 @@ def file_read(path: str) -> str:
2971
3532
  return f"Error reading '{path}': {e.strerror}"
2972
3533
 
2973
3534
  def file_write(path: str, content: str) -> str:
2974
- """Write content to a file in persistent storage. The path is relative to the storage root."""
3535
+ """Write a file to a mounted filesystem. Use the absolute path (e.g. /mnt/tools/data.txt)."""
2975
3536
  try:
2976
3537
  full_path = _safe_resolve(path)
2977
3538
  parent = os.path.dirname(full_path)
@@ -2985,48 +3546,75 @@ def file_write(path: str, content: str) -> str:
2985
3546
  except OSError as e:
2986
3547
  return f"Error writing '{path}': {e.strerror}"
2987
3548
 
2988
- def list_files(directory: str = "") -> str:
2989
- """List files in persistent storage. The directory is relative to the storage root."""
3549
+ def list_files(path: str) -> str:
3550
+ """List files in a mounted filesystem directory. Use the absolute path (e.g. /mnt/tools)."""
2990
3551
  try:
2991
- target = _safe_resolve(directory)
2992
- entries = os.listdir(target)
3552
+ full_path = _safe_resolve(path)
3553
+ entries = os.listdir(full_path)
2993
3554
  return "\\n".join(entries) if entries else "(empty directory)"
2994
3555
  except ValueError as e:
2995
3556
  return str(e)
2996
3557
  except OSError as e:
2997
- return f"Error listing '{directory}': {e.strerror}"
3558
+ return f"Error listing '{path}': {e.strerror}"
2998
3559
 
2999
3560
  tools.extend([
3000
- FunctionTool(file_read, description="Read a file from persistent storage. The path is relative to the storage root."),
3001
- FunctionTool(file_write, description="Write content to a file in persistent storage. The path is relative to the storage root."),
3002
- FunctionTool(list_files, description="List files in persistent storage. The directory is relative to the storage root."),
3561
+ FunctionTool(file_read, description="Read a file from a mounted filesystem. Use the absolute path (e.g. /mnt/tools/data.txt)."),
3562
+ FunctionTool(file_write, description="Write a file to a mounted filesystem. Use the absolute path (e.g. /mnt/tools/data.txt)."),
3563
+ FunctionTool(list_files, description="List files in a mounted filesystem directory. Use the absolute path (e.g. /mnt/tools)."),
3003
3564
  ])
3004
3565
  {{/if}}
3005
3566
 
3006
3567
  SYSTEM_MESSAGE = """
3007
3568
  You are a helpful assistant. Use tools when appropriate.
3008
- {{#if sessionStorageMountPath}}
3009
- You have persistent storage at {{sessionStorageMountPath}}. Use file tools to read and write files. Data persists across sessions.
3010
- {{/if}}
3569
+ {{#if needsOs}}
3570
+ You have access to the following mounted filesystems. Use file_read, file_write, and list_files with full absolute paths:
3571
+ {{#if sessionStorageMountPath}}- {{sessionStorageMountPath}}: ephemeral session storage (lost when session ends)
3572
+ {{/if}}{{#each efsMounts}}- {{mountPath}}: EFS persistent storage (persists across sessions and agent restarts)
3573
+ {{/each}}{{#each s3Mounts}}- {{mountPath}}: S3 Files persistent storage (durable, backed by S3)
3574
+ {{/each}}{{/if}}
3011
3575
  """
3012
3576
 
3013
- @app.entrypoint
3014
- async def invoke(payload, context):
3015
- log.info("Invoking Agent.....")
3577
+ # Reuses one AssistantAgent per session_id so each session keeps its own
3578
+ # in-process conversation history (best-effort; resets on cold start). Caches up
3579
+ # to 128 active sessions with LRU eviction (least-recently-used is dropped and
3580
+ # its history reset).
3581
+ _agents = OrderedDict()
3016
3582
 
3583
+
3584
+ async def get_or_create_agent(session_id):
3585
+ if session_id in _agents:
3586
+ _agents.move_to_end(session_id)
3587
+ return _agents[session_id]
3588
+ if len(_agents) >= 128:
3589
+ _agents.popitem(last=False)
3017
3590
  # Get MCP Tools
3018
3591
  mcp_tools = await get_streamable_http_mcp_tools()
3592
+ # Re-check after the await: a concurrent first-invocation for the same
3593
+ # session_id may have built and stored the agent while we were awaiting.
3594
+ # Don't overwrite it (that would orphan the agent the other request is using).
3595
+ if session_id not in _agents:
3596
+ _agents[session_id] = AssistantAgent(
3597
+ name="{{ name }}",
3598
+ model_client=load_model(),
3599
+ tools=tools + mcp_tools,
3600
+ system_message=SYSTEM_MESSAGE,
3601
+ )
3602
+ _agents.move_to_end(session_id)
3603
+ return _agents[session_id]
3019
3604
 
3020
- # Define an AssistantAgent with the model and tools
3021
- agent = AssistantAgent(
3022
- name="{{ name }}",
3023
- model_client=load_model(),
3024
- tools=tools + mcp_tools,
3025
- system_message=SYSTEM_MESSAGE,
3026
- )
3605
+
3606
+ @app.entrypoint
3607
+ async def invoke(payload, context):
3608
+ log.info("Invoking Agent.....")
3027
3609
 
3028
3610
  # Process the user prompt
3029
3611
  prompt = payload.get("prompt", "What can you help me with?")
3612
+ if not isinstance(prompt, str):
3613
+ raise ValueError("prompt must be a string")
3614
+ session_id = getattr(context, "session_id", "default-session")
3615
+
3616
+ # Reuse the per-session agent (preserves conversation history)
3617
+ agent = await get_or_create_agent(session_id)
3030
3618
 
3031
3619
  # Run the agent
3032
3620
  result = await agent.run(task=prompt)
@@ -3238,7 +3826,7 @@ dependencies = [
3238
3826
  "autogen-ext[mcp] >= 0.7.5",
3239
3827
  "opentelemetry-distro",
3240
3828
  "opentelemetry-exporter-otlp",
3241
- "bedrock-agentcore >= 1.0.3",
3829
+ "bedrock-agentcore >= 1.8.0",
3242
3830
  "botocore[crt] >= 1.35.0",
3243
3831
  "tiktoken",
3244
3832
  {{#if (eq modelProvider "Bedrock")}}
@@ -3276,6 +3864,11 @@ file defines a Starlette ASGI app with the Google ADK framework running within.
3276
3864
 
3277
3865
  \`model/load.py\` instantiates your chosen model provider (Gemini).
3278
3866
 
3867
+ ## Input Validation
3868
+
3869
+ Validate invocation input before forwarding it to the agent framework. Keep plain prompts typed as strings and pass
3870
+ only prompt text to the agent.
3871
+
3279
3872
  ## Environment Variables
3280
3873
 
3281
3874
  | Variable | Required | Description |
@@ -3287,7 +3880,8 @@ file defines a Starlette ASGI app with the Google ADK framework running within.
3287
3880
 
3288
3881
  If installation was successful, a virtual environment is already created with dependencies installed.
3289
3882
 
3290
- Run \`source .venv/bin/activate\` before developing.
3883
+ Activate the environment with \`source .venv/bin/activate\` on macOS/Linux, \`.venv\\Scripts\\activate.bat\` in Windows
3884
+ Command Prompt, or \`.\\.venv\\Scripts\\activate.ps1\` in Windows PowerShell.
3291
3885
 
3292
3886
  \`agentcore dev\` will start a local server on 0.0.0.0:8080.
3293
3887
 
@@ -3349,7 +3943,10 @@ Thumbs.db
3349
3943
  `;
3350
3944
 
3351
3945
  exports[`Assets Directory Snapshots > Python framework assets > python/python/http/googleadk/base/main.py should match snapshot 1`] = `
3352
- "import os
3946
+ "{{#if needsOs}}
3947
+ import os
3948
+ {{/if}}
3949
+ from collections import OrderedDict
3353
3950
  from google.adk.agents import Agent
3354
3951
  from google.adk.runners import Runner
3355
3952
  from google.adk.sessions import InMemorySessionService
@@ -3380,18 +3977,21 @@ def add_numbers(a: int, b: int) -> int:
3380
3977
  # Define a collection of tools used by the model
3381
3978
  tools = [add_numbers]
3382
3979
 
3383
- {{#if sessionStorageMountPath}}
3384
- SESSION_STORAGE_PATH = "{{sessionStorageMountPath}}"
3980
+ {{#if needsOs}}
3981
+ _MOUNT_PATHS = [
3982
+ {{#if sessionStorageMountPath}}"{{sessionStorageMountPath}}",{{/if}}
3983
+ {{#each efsMounts}}"{{mountPath}}",{{/each}}
3984
+ {{#each s3Mounts}}"{{mountPath}}",{{/each}}
3985
+ ]
3385
3986
 
3386
3987
  def _safe_resolve(path: str) -> str:
3387
- """Resolve path safely within the storage boundary."""
3388
- resolved = os.path.realpath(os.path.join(SESSION_STORAGE_PATH, path.lstrip("/")))
3389
- if not resolved.startswith(os.path.realpath(SESSION_STORAGE_PATH)):
3390
- raise ValueError(f"Path '{path}' is outside the storage boundary")
3988
+ resolved = os.path.realpath(path)
3989
+ if not any(resolved == os.path.realpath(m) or resolved.startswith(os.path.realpath(m) + os.sep) for m in _MOUNT_PATHS):
3990
+ raise ValueError(f"Path '{path}' is not within any configured mount ({', '.join(_MOUNT_PATHS)})")
3391
3991
  return resolved
3392
3992
 
3393
3993
  def file_read(path: str) -> str:
3394
- """Read a file from persistent storage. The path is relative to the storage root."""
3994
+ """Read a file from a mounted filesystem. Use the absolute path (e.g. /mnt/tools/data.txt)."""
3395
3995
  try:
3396
3996
  full_path = _safe_resolve(path)
3397
3997
  with open(full_path) as f:
@@ -3402,7 +4002,7 @@ def file_read(path: str) -> str:
3402
4002
  return f"Error reading '{path}': {e.strerror}"
3403
4003
 
3404
4004
  def file_write(path: str, content: str) -> str:
3405
- """Write content to a file in persistent storage. The path is relative to the storage root."""
4005
+ """Write a file to a mounted filesystem. Use the absolute path (e.g. /mnt/tools/data.txt)."""
3406
4006
  try:
3407
4007
  full_path = _safe_resolve(path)
3408
4008
  parent = os.path.dirname(full_path)
@@ -3416,25 +4016,28 @@ def file_write(path: str, content: str) -> str:
3416
4016
  except OSError as e:
3417
4017
  return f"Error writing '{path}': {e.strerror}"
3418
4018
 
3419
- def list_files(directory: str = "") -> str:
3420
- """List files in persistent storage. The directory is relative to the storage root."""
4019
+ def list_files(path: str) -> str:
4020
+ """List files in a mounted filesystem directory. Use the absolute path (e.g. /mnt/tools)."""
3421
4021
  try:
3422
- target = _safe_resolve(directory)
3423
- entries = os.listdir(target)
4022
+ full_path = _safe_resolve(path)
4023
+ entries = os.listdir(full_path)
3424
4024
  return "\\n".join(entries) if entries else "(empty directory)"
3425
4025
  except ValueError as e:
3426
4026
  return str(e)
3427
4027
  except OSError as e:
3428
- return f"Error listing '{directory}': {e.strerror}"
4028
+ return f"Error listing '{path}': {e.strerror}"
3429
4029
 
3430
4030
  tools.extend([file_read, file_write, list_files])
3431
4031
  {{/if}}
3432
4032
 
3433
4033
  AGENT_INSTRUCTION = """
3434
4034
  I can answer your questions using the knowledge I have!
3435
- {{#if sessionStorageMountPath}}
3436
- You have persistent storage at {{sessionStorageMountPath}}. Use file tools to read and write files. Data persists across sessions.
3437
- {{/if}}
4035
+ {{#if needsOs}}
4036
+ You have access to the following mounted filesystems. Use file_read, file_write, and list_files with full absolute paths:
4037
+ {{#if sessionStorageMountPath}}- {{sessionStorageMountPath}}: ephemeral session storage (lost when session ends)
4038
+ {{/if}}{{#each efsMounts}}- {{mountPath}}: EFS persistent storage (persists across sessions and agent restarts)
4039
+ {{/each}}{{#each s3Mounts}}- {{mountPath}}: S3 Files persistent storage (durable, backed by S3)
4040
+ {{/each}}{{/if}}
3438
4041
  """
3439
4042
 
3440
4043
  # Get MCP Toolset
@@ -3464,21 +4067,57 @@ agent = Agent(
3464
4067
  )
3465
4068
 
3466
4069
 
3467
- # Session and Runner
3468
- async def setup_session_and_runner(user_id, session_id):
3469
- ensure_credentials_loaded()
3470
- session_service = InMemorySessionService()
3471
- session = await session_service.create_session(
4070
+ # Module-level session service and runner preserve history across invocations.
4071
+ # InMemorySessionService retains every (app_name, user_id, session_id) triple
4072
+ # forever, so we bound it to 128 active sessions with LRU eviction (the
4073
+ # least-recently-used session is deleted and its history reset) to keep a
4074
+ # long-running process from growing without limit. For durable history, swap in
4075
+ # a persistent session service (e.g. DatabaseSessionService).
4076
+ _SESSION_LIMIT = 128
4077
+ _session_service = InMemorySessionService()
4078
+ _session_keys = OrderedDict()
4079
+ _runner = None
4080
+
4081
+
4082
+ def get_or_create_runner():
4083
+ global _runner
4084
+ if _runner is None:
4085
+ ensure_credentials_loaded()
4086
+ _runner = Runner(
4087
+ agent=agent,
4088
+ app_name=APP_NAME,
4089
+ session_service=_session_service,
4090
+ )
4091
+ return _runner
4092
+
4093
+
4094
+ async def get_or_create_session(user_id, session_id):
4095
+ key = (user_id, session_id)
4096
+ if key in _session_keys:
4097
+ _session_keys.move_to_end(key)
4098
+ else:
4099
+ while len(_session_keys) >= _SESSION_LIMIT:
4100
+ (old_user_id, old_session_id), _ = _session_keys.popitem(last=False)
4101
+ await _session_service.delete_session(
4102
+ app_name=APP_NAME, user_id=old_user_id, session_id=old_session_id
4103
+ )
4104
+ _session_keys[key] = True
4105
+
4106
+ session = await _session_service.get_session(
3472
4107
  app_name=APP_NAME, user_id=user_id, session_id=session_id
3473
4108
  )
3474
- runner = Runner(agent=agent, app_name=APP_NAME, session_service=session_service)
3475
- return session, runner
4109
+ if session is None:
4110
+ session = await _session_service.create_session(
4111
+ app_name=APP_NAME, user_id=user_id, session_id=session_id
4112
+ )
4113
+ return session
3476
4114
 
3477
4115
 
3478
4116
  # Agent Interaction
3479
4117
  async def call_agent_async(query, user_id, session_id):
3480
4118
  content = types.Content(role="user", parts=[types.Part(text=query)])
3481
- session, runner = await setup_session_and_runner(user_id, session_id)
4119
+ runner = get_or_create_runner()
4120
+ session = await get_or_create_session(user_id, session_id)
3482
4121
  events = runner.run_async(
3483
4122
  user_id=user_id, session_id=session.id, new_message=content
3484
4123
  )
@@ -3497,6 +4136,8 @@ async def invoke(payload, context):
3497
4136
 
3498
4137
  # Process the user prompt
3499
4138
  prompt = payload.get("prompt", "What can you help me with?")
4139
+ if not isinstance(prompt, str):
4140
+ raise ValueError("prompt must be a string")
3500
4141
  session_id = getattr(context, "session_id", "default_session")
3501
4142
  user_id = payload.get("user_id", "default_user")
3502
4143
 
@@ -3554,20 +4195,20 @@ def get_all_gateway_mcp_toolsets() -> list[MCPToolset]:
3554
4195
  {{#each gatewayProviders}}
3555
4196
  url = os.environ.get("{{envVarName}}")
3556
4197
  if url:
3557
- {{#if (eq authType "AWS_IAM")}}
4198
+ {{#if (eq authType "AWS_IAM")}}
3558
4199
  session = create_aws_session()
3559
4200
  auth = SigV4HTTPXAuth(session.get_credentials(), "bedrock-agentcore", session.region_name)
3560
- toolsets.append(MCPToolset(connection_params=StreamableHTTPConnectionParams(
4201
+ toolsets.append(MCPToolset(tool_name_prefix="{{snakeCase name}}", connection_params=StreamableHTTPConnectionParams(
3561
4202
  url=url,
3562
4203
  httpx_client_factory=lambda **kwargs: httpx.AsyncClient(auth=auth, **kwargs)
3563
4204
  )))
3564
- {{else if (eq authType "CUSTOM_JWT")}}
4205
+ {{else if (eq authType "CUSTOM_JWT")}}
3565
4206
  token = _get_bearer_token_{{snakeCase name}}()
3566
4207
  headers = {"Authorization": f"Bearer {token}"} if token else None
3567
- toolsets.append(MCPToolset(connection_params=StreamableHTTPConnectionParams(url=url, headers=headers)))
3568
- {{else}}
3569
- toolsets.append(MCPToolset(connection_params=StreamableHTTPConnectionParams(url=url)))
3570
- {{/if}}
4208
+ toolsets.append(MCPToolset(tool_name_prefix="{{snakeCase name}}", connection_params=StreamableHTTPConnectionParams(url=url, headers=headers)))
4209
+ {{else}}
4210
+ toolsets.append(MCPToolset(tool_name_prefix="{{snakeCase name}}", connection_params=StreamableHTTPConnectionParams(url=url)))
4211
+ {{/if}}
3571
4212
  else:
3572
4213
  logger.warning("{{envVarName}} not set — {{name}} gateway tools unavailable")
3573
4214
  {{/each}}
@@ -3658,9 +4299,11 @@ description = "AgentCore Runtime Application using Google ADK"
3658
4299
  readme = "README.md"
3659
4300
  requires-python = ">=3.10"
3660
4301
  dependencies = [
3661
- "opentelemetry-distro",
3662
- "opentelemetry-exporter-otlp",
3663
- "google-adk >= 1.17.0",
4302
+ "aws-opentelemetry-distro >= 0.17.0",
4303
+ "google-adk >= 1.35.0, < 2.0.0",
4304
+ "google-genai >= 1.0.0, < 2.0.0",
4305
+ # 1.13.0 was yanked for broken imports.
4306
+ "opentelemetry-resourcedetector-gcp >= 1.9.0a0, < 2.0.0, != 1.13.0",
3664
4307
  "bedrock-agentcore >= 1.0.3",
3665
4308
  "botocore[crt] >= 1.35.0",
3666
4309
  {{#if hasGateway}}{{#if (includes gatewayAuthTypes "AWS_IAM")}}"mcp-proxy-for-aws >= 1.1.0",
@@ -3688,6 +4331,11 @@ file defines a Starlette ASGI app with the LangChain/LangGraph framework running
3688
4331
 
3689
4332
  \`model/load.py\` instantiates your chosen model provider.
3690
4333
 
4334
+ ## Input Validation
4335
+
4336
+ Validate invocation input before forwarding it to the agent framework. Keep plain prompts typed as strings and pass
4337
+ only prompt text to the agent.
4338
+
3691
4339
  ## Environment Variables
3692
4340
 
3693
4341
  | Variable | Required | Description |
@@ -3699,7 +4347,8 @@ file defines a Starlette ASGI app with the LangChain/LangGraph framework running
3699
4347
 
3700
4348
  If installation was successful, a virtual environment is already created with dependencies installed.
3701
4349
 
3702
- Run \`source .venv/bin/activate\` before developing.
4350
+ Activate the environment with \`source .venv/bin/activate\` on macOS/Linux, \`.venv\\Scripts\\activate.bat\` in Windows
4351
+ Command Prompt, or \`.\\.venv\\Scripts\\activate.ps1\` in Windows PowerShell.
3703
4352
 
3704
4353
  \`agentcore dev\` will start a local server on 0.0.0.0:8080.
3705
4354
 
@@ -3761,10 +4410,20 @@ Thumbs.db
3761
4410
  `;
3762
4411
 
3763
4412
  exports[`Assets Directory Snapshots > Python framework assets > python/python/http/langchain_langgraph/base/main.py should match snapshot 1`] = `
3764
- "import os
3765
- from langchain_core.messages import HumanMessage
4413
+ "{{#if needsOs}}
4414
+ import os
4415
+ {{/if}}
4416
+ from collections import OrderedDict
4417
+ from typing import Any
4418
+
4419
+ from langchain_core.messages import HumanMessage{{#if hasConfigBundle}}, SystemMessage{{/if}}
4420
+ from langgraph.checkpoint.memory import InMemorySaver
3766
4421
  from langgraph.prebuilt import create_react_agent
3767
4422
  from langchain.tools import tool
4423
+ {{#if hasConfigBundle}}
4424
+ from langchain_core.callbacks import BaseCallbackHandler
4425
+ from bedrock_agentcore.runtime.context import BedrockAgentCoreContext
4426
+ {{/if}}
3768
4427
  from opentelemetry.instrumentation.langchain import LangchainInstrumentor
3769
4428
  from bedrock_agentcore.runtime import BedrockAgentCoreApp
3770
4429
  from model.load import load_model
@@ -3788,6 +4447,17 @@ def get_or_create_model():
3788
4447
  return _llm
3789
4448
 
3790
4449
 
4450
+ DEFAULT_SYSTEM_PROMPT = """
4451
+ You are a helpful assistant. Use tools when appropriate.
4452
+ {{#if needsOs}}
4453
+ You have access to the following mounted filesystems. Use file_read, file_write, and list_files with full absolute paths:
4454
+ {{#if sessionStorageMountPath}}- {{sessionStorageMountPath}}: ephemeral session storage (lost when session ends)
4455
+ {{/if}}{{#each efsMounts}}- {{mountPath}}: EFS persistent storage (persists across sessions and agent restarts)
4456
+ {{/each}}{{#each s3Mounts}}- {{mountPath}}: S3 Files persistent storage (durable, backed by S3)
4457
+ {{/each}}{{/if}}
4458
+ """
4459
+
4460
+
3791
4461
  # Define a simple function tool
3792
4462
  @tool
3793
4463
  def add_numbers(a: int, b: int) -> int:
@@ -3798,19 +4468,42 @@ def add_numbers(a: int, b: int) -> int:
3798
4468
  # Define a collection of tools used by the model
3799
4469
  tools = [add_numbers]
3800
4470
 
3801
- {{#if sessionStorageMountPath}}
3802
- SESSION_STORAGE_PATH = "{{sessionStorageMountPath}}"
4471
+ # Module-level checkpointer preserves conversation history across invocations.
4472
+ # InMemorySaver keeps every thread_id (= session_id) checkpoint in memory
4473
+ # forever, so we bound it to 128 active threads with LRU eviction (the
4474
+ # least-recently-used thread is deleted and its history reset) to keep a
4475
+ # long-running process from growing without limit. For durable history, swap in
4476
+ # a persistent checkpointer (e.g. SqliteSaver/AsyncSqliteSaver with a file path).
4477
+ _CHECKPOINT_LIMIT = 128
4478
+ _checkpointer = InMemorySaver()
4479
+ _thread_ids = OrderedDict()
4480
+
4481
+
4482
+ def touch_thread(thread_id):
4483
+ if thread_id in _thread_ids:
4484
+ _thread_ids.move_to_end(thread_id)
4485
+ return
4486
+ while len(_thread_ids) >= _CHECKPOINT_LIMIT:
4487
+ evicted, _ = _thread_ids.popitem(last=False)
4488
+ _checkpointer.delete_thread(evicted)
4489
+ _thread_ids[thread_id] = True
4490
+
4491
+ {{#if needsOs}}
4492
+ _MOUNT_PATHS = [
4493
+ {{#if sessionStorageMountPath}}"{{sessionStorageMountPath}}",{{/if}}
4494
+ {{#each efsMounts}}"{{mountPath}}",{{/each}}
4495
+ {{#each s3Mounts}}"{{mountPath}}",{{/each}}
4496
+ ]
3803
4497
 
3804
4498
  def _safe_resolve(path: str) -> str:
3805
- """Resolve path safely within the storage boundary."""
3806
- resolved = os.path.realpath(os.path.join(SESSION_STORAGE_PATH, path.lstrip("/")))
3807
- if not resolved.startswith(os.path.realpath(SESSION_STORAGE_PATH)):
3808
- raise ValueError(f"Path '{path}' is outside the storage boundary")
4499
+ resolved = os.path.realpath(path)
4500
+ if not any(resolved == os.path.realpath(m) or resolved.startswith(os.path.realpath(m) + os.sep) for m in _MOUNT_PATHS):
4501
+ raise ValueError(f"Path '{path}' is not within any configured mount ({', '.join(_MOUNT_PATHS)})")
3809
4502
  return resolved
3810
4503
 
3811
4504
  @tool
3812
4505
  def file_read(path: str) -> str:
3813
- """Read a file from persistent storage. The path is relative to the storage root."""
4506
+ """Read a file from a mounted filesystem. Use the absolute path (e.g. /mnt/tools/data.txt)."""
3814
4507
  try:
3815
4508
  full_path = _safe_resolve(path)
3816
4509
  with open(full_path) as f:
@@ -3822,7 +4515,7 @@ def file_read(path: str) -> str:
3822
4515
 
3823
4516
  @tool
3824
4517
  def file_write(path: str, content: str) -> str:
3825
- """Write content to a file in persistent storage. The path is relative to the storage root."""
4518
+ """Write a file to a mounted filesystem. Use the absolute path (e.g. /mnt/tools/data.txt)."""
3826
4519
  try:
3827
4520
  full_path = _safe_resolve(path)
3828
4521
  parent = os.path.dirname(full_path)
@@ -3837,27 +4530,42 @@ def file_write(path: str, content: str) -> str:
3837
4530
  return f"Error writing '{path}': {e.strerror}"
3838
4531
 
3839
4532
  @tool
3840
- def list_files(directory: str = "") -> str:
3841
- """List files in persistent storage. The directory is relative to the storage root."""
4533
+ def list_files(path: str) -> str:
4534
+ """List files in a mounted filesystem directory. Use the absolute path (e.g. /mnt/tools)."""
3842
4535
  try:
3843
- target = _safe_resolve(directory)
3844
- entries = os.listdir(target)
4536
+ full_path = _safe_resolve(path)
4537
+ entries = os.listdir(full_path)
3845
4538
  return "\\n".join(entries) if entries else "(empty directory)"
3846
4539
  except ValueError as e:
3847
4540
  return str(e)
3848
4541
  except OSError as e:
3849
- return f"Error listing '{directory}': {e.strerror}"
4542
+ return f"Error listing '{path}': {e.strerror}"
3850
4543
 
3851
4544
  tools.extend([file_read, file_write, list_files])
3852
4545
  {{/if}}
3853
4546
 
3854
- SYSTEM_PROMPT = """
3855
- You are a helpful assistant. Use tools when appropriate.
3856
- {{#if sessionStorageMountPath}}
3857
- You have persistent storage at {{sessionStorageMountPath}}. Use file tools to read and write files. Data persists across sessions.
3858
- {{/if}}
3859
- """
4547
+ {{#if hasConfigBundle}}
4548
+
4549
+ class ConfigBundleCallback(BaseCallbackHandler):
4550
+ """Injects config bundle values into LangGraph agent at runtime.
4551
+
4552
+ BedrockAgentCoreContext.get_config_bundle() fetches the component configuration
4553
+ for the current runtime ARN from the config bundle service. The SDK caches the
4554
+ result and refreshes on bundle version changes.
4555
+ """
3860
4556
 
4557
+ def on_chain_start(self, serialized: dict, inputs: dict, **kwargs: Any) -> None:
4558
+ config = BedrockAgentCoreContext.get_config_bundle()
4559
+ prompt = config.get("systemPrompt", DEFAULT_SYSTEM_PROMPT)
4560
+
4561
+ messages = inputs.get("messages", [])
4562
+ if messages and isinstance(messages[0], SystemMessage):
4563
+ messages[0] = SystemMessage(content=prompt)
4564
+ else:
4565
+ messages.insert(0, SystemMessage(content=prompt))
4566
+ inputs["messages"] = messages
4567
+
4568
+ {{/if}}
3861
4569
 
3862
4570
  @app.entrypoint
3863
4571
  async def invoke(payload, context):
@@ -3875,15 +4583,51 @@ async def invoke(payload, context):
3875
4583
  if mcp_client:
3876
4584
  mcp_tools = await mcp_client.get_tools()
3877
4585
 
3878
- # Define the agent using create_react_agent
3879
- graph = create_react_agent(get_or_create_model(), tools=mcp_tools + tools, prompt=SYSTEM_PROMPT)
4586
+ # Define the agent using create_react_agent (checkpointer is shared across invocations)
4587
+ {{#if hasConfigBundle}}
4588
+ graph = create_react_agent(
4589
+ get_or_create_model(),
4590
+ tools=mcp_tools + tools,
4591
+ prompt=DEFAULT_SYSTEM_PROMPT,
4592
+ checkpointer=_checkpointer,
4593
+ )
4594
+ callback = ConfigBundleCallback()
3880
4595
 
3881
4596
  # Process the user prompt
3882
4597
  prompt = payload.get("prompt", "What can you help me with?")
4598
+ if not isinstance(prompt, str):
4599
+ raise ValueError("prompt must be a string")
4600
+ session_id = getattr(context, "session_id", "default-session")
4601
+ touch_thread(session_id)
3883
4602
  log.info(f"Agent input: {prompt}")
3884
4603
 
3885
- # Run the agent
3886
- result = await graph.ainvoke({"messages": [HumanMessage(content=prompt)]})
4604
+ # Run the agent with config bundle callback (checkpointer auto-loads/saves history per session)
4605
+ result = await graph.ainvoke(
4606
+ {"messages": [HumanMessage(content=prompt)]},
4607
+ config={"callbacks": [callback], "configurable": {"thread_id": session_id}},
4608
+ )
4609
+ {{else}}
4610
+ graph = create_react_agent(
4611
+ get_or_create_model(),
4612
+ tools=mcp_tools + tools,
4613
+ prompt=DEFAULT_SYSTEM_PROMPT,
4614
+ checkpointer=_checkpointer,
4615
+ )
4616
+
4617
+ # Process the user prompt
4618
+ prompt = payload.get("prompt", "What can you help me with?")
4619
+ if not isinstance(prompt, str):
4620
+ raise ValueError("prompt must be a string")
4621
+ session_id = getattr(context, "session_id", "default-session")
4622
+ touch_thread(session_id)
4623
+ log.info(f"Agent input: {prompt}")
4624
+
4625
+ # Run the agent (checkpointer auto-loads/saves history per session)
4626
+ result = await graph.ainvoke(
4627
+ {"messages": [HumanMessage(content=prompt)]},
4628
+ config={"configurable": {"thread_id": session_id}},
4629
+ )
4630
+ {{/if}}
3887
4631
 
3888
4632
  # Return result
3889
4633
  output = result["messages"][-1].content
@@ -3936,23 +4680,25 @@ def get_all_gateway_mcp_client() -> MultiServerMCPClient | None:
3936
4680
  {{#each gatewayProviders}}
3937
4681
  url = os.environ.get("{{envVarName}}")
3938
4682
  if url:
3939
- {{#if (eq authType "AWS_IAM")}}
4683
+ {{#if (eq authType "AWS_IAM")}}
3940
4684
  session = create_aws_session()
3941
4685
  auth = SigV4HTTPXAuth(session.get_credentials(), "bedrock-agentcore", session.region_name)
3942
- servers["{{name}}"] = {"transport": "streamable_http", "url": url, "auth": auth}
3943
- {{else if (eq authType "CUSTOM_JWT")}}
4686
+ servers["{{snakeCase name}}"] = {"transport": "streamable_http", "url": url, "auth": auth}
4687
+ {{else if (eq authType "CUSTOM_JWT")}}
3944
4688
  token = _get_bearer_token_{{snakeCase name}}()
3945
4689
  headers = {"Authorization": f"Bearer {token}"} if token else None
3946
- servers["{{name}}"] = {"transport": "streamable_http", "url": url, "headers": headers}
3947
- {{else}}
3948
- servers["{{name}}"] = {"transport": "streamable_http", "url": url}
3949
- {{/if}}
4690
+ servers["{{snakeCase name}}"] = {"transport": "streamable_http", "url": url, "headers": headers}
4691
+ {{else}}
4692
+ servers["{{snakeCase name}}"] = {"transport": "streamable_http", "url": url}
4693
+ {{/if}}
3950
4694
  else:
3951
4695
  logger.warning("{{envVarName}} not set — {{name}} gateway tools unavailable")
3952
4696
  {{/each}}
3953
4697
  if not servers:
3954
4698
  return None
3955
- return MultiServerMCPClient(servers)
4699
+ # tool_name_prefix namespaces each gateway's tools by server key so multiple
4700
+ # gateways exposing the same tool (e.g. x_amz_bedrock_agentcore_search) don't collide.
4701
+ return MultiServerMCPClient(servers, tool_name_prefix=True)
3956
4702
  {{else}}
3957
4703
  {{#if isVpc}}
3958
4704
  # VPC mode: external MCP endpoints are not reachable without a NAT gateway.
@@ -4126,13 +4872,13 @@ description = "AgentCore Runtime Application using LangChain/LangGraph"
4126
4872
  readme = "README.md"
4127
4873
  requires-python = ">=3.10"
4128
4874
  dependencies = [
4129
- "aws-opentelemetry-distro",
4875
+ "aws-opentelemetry-distro ~= 0.18.0",
4130
4876
  "opentelemetry-instrumentation-langchain >= 0.59.0",
4131
4877
  "langgraph >= 1.0.2",
4132
- "mcp >= 1.19.0",
4133
- "langchain-mcp-adapters >= 0.1.11",
4878
+ "mcp ~= 1.24.0",
4879
+ "langchain-mcp-adapters >= 0.3.1",
4134
4880
  "langchain >= 1.0.3",
4135
- "bedrock-agentcore >= 1.0.3",
4881
+ "bedrock-agentcore >= 1.8.0",
4136
4882
  "botocore[crt] >= 1.35.0",
4137
4883
  {{#if (eq modelProvider "Bedrock")}}
4138
4884
  "langchain-aws >= 1.0.0",
@@ -4145,6 +4891,7 @@ dependencies = [
4145
4891
  {{/if}}
4146
4892
  {{#if (eq modelProvider "Gemini")}}
4147
4893
  "langchain-google-genai >= 3.0.3",
4894
+ "google-genai >= 1.0.0, < 2.0.0",
4148
4895
  {{/if}}
4149
4896
  {{#if hasGateway}}{{#if (includes gatewayAuthTypes "AWS_IAM")}}"mcp-proxy-for-aws >= 1.1.0",
4150
4897
  {{/if}}{{/if}}
@@ -4171,6 +4918,11 @@ file defines a Starlette ASGI app with the OpenAI Agents SDK framework running w
4171
4918
 
4172
4919
  \`model/load.py\` instantiates your chosen model provider (OpenAI).
4173
4920
 
4921
+ ## Input Validation
4922
+
4923
+ Validate invocation input before forwarding it to the agent framework. Keep plain prompts typed as strings and pass
4924
+ only prompt text to the agent.
4925
+
4174
4926
  ## Environment Variables
4175
4927
 
4176
4928
  | Variable | Required | Description |
@@ -4182,7 +4934,8 @@ file defines a Starlette ASGI app with the OpenAI Agents SDK framework running w
4182
4934
 
4183
4935
  If installation was successful, a virtual environment is already created with dependencies installed.
4184
4936
 
4185
- Run \`source .venv/bin/activate\` before developing.
4937
+ Activate the environment with \`source .venv/bin/activate\` on macOS/Linux, \`.venv\\Scripts\\activate.bat\` in Windows
4938
+ Command Prompt, or \`.\\.venv\\Scripts\\activate.ps1\` in Windows PowerShell.
4186
4939
 
4187
4940
  \`agentcore dev\` will start a local server on 0.0.0.0:8080.
4188
4941
 
@@ -4244,8 +4997,14 @@ Thumbs.db
4244
4997
  `;
4245
4998
 
4246
4999
  exports[`Assets Directory Snapshots > Python framework assets > python/python/http/openaiagents/base/main.py should match snapshot 1`] = `
4247
- "import os
4248
- from agents import Agent, Runner, function_tool
5000
+ "{{#if needsOs}}
5001
+ import os
5002
+ {{/if}}
5003
+ {{#if hasGateway}}
5004
+ from contextlib import AsyncExitStack
5005
+ {{/if}}
5006
+ from functools import lru_cache
5007
+ from agents import Agent, Runner, SQLiteSession, function_tool
4249
5008
  from bedrock_agentcore.runtime import BedrockAgentCoreApp
4250
5009
  from model.load import load_model
4251
5010
  {{#if hasGateway}}
@@ -4283,19 +5042,22 @@ def add_numbers(a: int, b: int) -> int:
4283
5042
 
4284
5043
  tools = [add_numbers]
4285
5044
 
4286
- {{#if sessionStorageMountPath}}
4287
- SESSION_STORAGE_PATH = "{{sessionStorageMountPath}}"
5045
+ {{#if needsOs}}
5046
+ _MOUNT_PATHS = [
5047
+ {{#if sessionStorageMountPath}}"{{sessionStorageMountPath}}",{{/if}}
5048
+ {{#each efsMounts}}"{{mountPath}}",{{/each}}
5049
+ {{#each s3Mounts}}"{{mountPath}}",{{/each}}
5050
+ ]
4288
5051
 
4289
5052
  def _safe_resolve(path: str) -> str:
4290
- """Resolve path safely within the storage boundary."""
4291
- resolved = os.path.realpath(os.path.join(SESSION_STORAGE_PATH, path.lstrip("/")))
4292
- if not resolved.startswith(os.path.realpath(SESSION_STORAGE_PATH)):
4293
- raise ValueError(f"Path '{path}' is outside the storage boundary")
5053
+ resolved = os.path.realpath(path)
5054
+ if not any(resolved == os.path.realpath(m) or resolved.startswith(os.path.realpath(m) + os.sep) for m in _MOUNT_PATHS):
5055
+ raise ValueError(f"Path '{path}' is not within any configured mount ({', '.join(_MOUNT_PATHS)})")
4294
5056
  return resolved
4295
5057
 
4296
5058
  @function_tool
4297
5059
  def file_read(path: str) -> str:
4298
- """Read a file from persistent storage. The path is relative to the storage root."""
5060
+ """Read a file from a mounted filesystem. Use the absolute path (e.g. /mnt/tools/data.txt)."""
4299
5061
  try:
4300
5062
  full_path = _safe_resolve(path)
4301
5063
  with open(full_path) as f:
@@ -4307,7 +5069,7 @@ def file_read(path: str) -> str:
4307
5069
 
4308
5070
  @function_tool
4309
5071
  def file_write(path: str, content: str) -> str:
4310
- """Write content to a file in persistent storage. The path is relative to the storage root."""
5072
+ """Write a file to a mounted filesystem. Use the absolute path (e.g. /mnt/tools/data.txt)."""
4311
5073
  try:
4312
5074
  full_path = _safe_resolve(path)
4313
5075
  parent = os.path.dirname(full_path)
@@ -4322,42 +5084,60 @@ def file_write(path: str, content: str) -> str:
4322
5084
  return f"Error writing '{path}': {e.strerror}"
4323
5085
 
4324
5086
  @function_tool
4325
- def list_files(directory: str = "") -> str:
4326
- """List files in persistent storage. The directory is relative to the storage root."""
5087
+ def list_files(path: str) -> str:
5088
+ """List files in a mounted filesystem directory. Use the absolute path (e.g. /mnt/tools)."""
4327
5089
  try:
4328
- target = _safe_resolve(directory)
4329
- entries = os.listdir(target)
5090
+ full_path = _safe_resolve(path)
5091
+ entries = os.listdir(full_path)
4330
5092
  return "\\n".join(entries) if entries else "(empty directory)"
4331
5093
  except ValueError as e:
4332
5094
  return str(e)
4333
5095
  except OSError as e:
4334
- return f"Error listing '{directory}': {e.strerror}"
5096
+ return f"Error listing '{path}': {e.strerror}"
4335
5097
 
4336
5098
  tools.extend([file_read, file_write, list_files])
4337
5099
  {{/if}}
4338
5100
 
4339
5101
  INSTRUCTIONS = """
4340
5102
  You are a helpful assistant. Use tools when appropriate.
4341
- {{#if sessionStorageMountPath}}
4342
- You have persistent storage at {{sessionStorageMountPath}}. Use file tools to read and write files. Data persists across sessions.
4343
- {{/if}}
5103
+ {{#if needsOs}}
5104
+ You have access to the following mounted filesystems. Use file_read, file_write, and list_files with full absolute paths:
5105
+ {{#if sessionStorageMountPath}}- {{sessionStorageMountPath}}: ephemeral session storage (lost when session ends)
5106
+ {{/if}}{{#each efsMounts}}- {{mountPath}}: EFS persistent storage (persists across sessions and agent restarts)
5107
+ {{/each}}{{#each s3Mounts}}- {{mountPath}}: S3 Files persistent storage (durable, backed by S3)
5108
+ {{/each}}{{/if}}
4344
5109
  """
4345
5110
 
5111
+ # Caches up to 128 active sessions; LRU eviction silently resets history for
5112
+ # the oldest session. For production use, replace with a durable session store
5113
+ # (e.g. SQLiteSession with a file path).
5114
+ @lru_cache(maxsize=128)
5115
+ def get_session(session_id):
5116
+ return SQLiteSession(session_id)
5117
+
5118
+
4346
5119
  # Define the agent execution
4347
- async def main(query):
5120
+ async def main(query, session):
4348
5121
  ensure_credentials_loaded()
4349
5122
  try:
4350
5123
  {{#if hasGateway}}
4351
5124
  if mcp_servers:
4352
- agent = Agent(
4353
- name="{{ name }}",
4354
- model="gpt-4.1",
4355
- instructions=INSTRUCTIONS,
4356
- mcp_servers=mcp_servers,
4357
- tools=tools
4358
- )
4359
- result = await Runner.run(agent, query)
4360
- return result
5125
+ # Connect every gateway server before the run; include_server_in_tool_names
5126
+ # namespaces each server's tools so multiple gateways exposing the same tool
5127
+ # (e.g. x_amz_bedrock_agentcore_search) don't collide.
5128
+ async with AsyncExitStack() as stack:
5129
+ for server in mcp_servers:
5130
+ await stack.enter_async_context(server)
5131
+ agent = Agent(
5132
+ name="{{ name }}",
5133
+ model="gpt-4.1",
5134
+ instructions=INSTRUCTIONS,
5135
+ mcp_servers=mcp_servers,
5136
+ tools=tools,
5137
+ mcp_config={"include_server_in_tool_names": True},
5138
+ )
5139
+ result = await Runner.run(agent, query, session=session)
5140
+ return result
4361
5141
  else:
4362
5142
  agent = Agent(
4363
5143
  name="{{ name }}",
@@ -4366,7 +5146,7 @@ async def main(query):
4366
5146
  mcp_servers=[],
4367
5147
  tools=tools
4368
5148
  )
4369
- result = await Runner.run(agent, query)
5149
+ result = await Runner.run(agent, query, session=session)
4370
5150
  return result
4371
5151
  {{else}}
4372
5152
  if mcp_servers:
@@ -4379,7 +5159,7 @@ async def main(query):
4379
5159
  mcp_servers=active_servers,
4380
5160
  tools=tools
4381
5161
  )
4382
- result = await Runner.run(agent, query)
5162
+ result = await Runner.run(agent, query, session=session)
4383
5163
  return result
4384
5164
  else:
4385
5165
  agent = Agent(
@@ -4389,7 +5169,7 @@ async def main(query):
4389
5169
  mcp_servers=[],
4390
5170
  tools=tools
4391
5171
  )
4392
- result = await Runner.run(agent, query)
5172
+ result = await Runner.run(agent, query, session=session)
4393
5173
  return result
4394
5174
  {{/if}}
4395
5175
  except Exception as e:
@@ -4403,9 +5183,13 @@ async def invoke(payload, context):
4403
5183
 
4404
5184
  # Process the user prompt
4405
5185
  prompt = payload.get("prompt", "What can you help me with?")
5186
+ if not isinstance(prompt, str):
5187
+ raise ValueError("prompt must be a string")
5188
+ session_id = getattr(context, "session_id", "default-session")
5189
+ session = get_session(session_id)
4406
5190
 
4407
- # Run the agent
4408
- result = await main(prompt)
5191
+ # Run the agent (session automatically loads/saves conversation history)
5192
+ result = await main(prompt, session)
4409
5193
 
4410
5194
  # Return result
4411
5195
  return {"result": result.final_output}
@@ -4457,20 +5241,20 @@ def get_all_gateway_mcp_servers() -> list[MCPServerStreamableHttp]:
4457
5241
  {{#each gatewayProviders}}
4458
5242
  url = os.environ.get("{{envVarName}}")
4459
5243
  if url:
4460
- {{#if (eq authType "AWS_IAM")}}
5244
+ {{#if (eq authType "AWS_IAM")}}
4461
5245
  session = create_aws_session()
4462
5246
  auth = SigV4HTTPXAuth(session.get_credentials(), "bedrock-agentcore", session.region_name)
4463
5247
  servers.append(MCPServerStreamableHttp(
4464
5248
  name="{{name}}",
4465
5249
  params={"url": url, "httpx_client_factory": lambda **kwargs: httpx.AsyncClient(auth=auth, **kwargs)}
4466
5250
  ))
4467
- {{else if (eq authType "CUSTOM_JWT")}}
5251
+ {{else if (eq authType "CUSTOM_JWT")}}
4468
5252
  token = _get_bearer_token_{{snakeCase name}}()
4469
5253
  headers = {"Authorization": f"Bearer {token}"} if token else {}
4470
5254
  servers.append(MCPServerStreamableHttp(name="{{name}}", params={"url": url, "headers": headers}))
4471
- {{else}}
5255
+ {{else}}
4472
5256
  servers.append(MCPServerStreamableHttp(name="{{name}}", params={"url": url}))
4473
- {{/if}}
5257
+ {{/if}}
4474
5258
  else:
4475
5259
  logger.warning("{{envVarName}} not set — {{name}} gateway tools unavailable")
4476
5260
  {{/each}}
@@ -4557,9 +5341,9 @@ description = "AgentCore Runtime Application using OpenAI Agents SDK"
4557
5341
  readme = "README.md"
4558
5342
  requires-python = ">=3.10"
4559
5343
  dependencies = [
4560
- "aws-opentelemetry-distro",
4561
- "openai-agents >= 0.4.2",
4562
- "bedrock-agentcore >= 1.0.3",
5344
+ "aws-opentelemetry-distro ~= 0.18.0",
5345
+ "openai-agents >= 0.16.0",
5346
+ "bedrock-agentcore >= 1.8.0",
4563
5347
  "botocore[crt] >= 1.35.0",
4564
5348
  {{#if hasGateway}}{{#if (includes gatewayAuthTypes "AWS_IAM")}}"mcp-proxy-for-aws >= 1.1.0",
4565
5349
  {{/if}}{{/if}}
@@ -4586,6 +5370,12 @@ file defines a Starlette ASGI app with the chosen Agent framework SDK running wi
4586
5370
 
4587
5371
  \`model/load.py\` instantiates your chosen model provider.
4588
5372
 
5373
+ ## Input Validation
5374
+
5375
+ Validate invocation input before forwarding it to Strands. Keep plain prompts typed as strings. If the app accepts a
5376
+ caller-supplied message history, retain \`strip_trailing_tool_use()\`, which normalizes the history tail before
5377
+ invoking the agent.
5378
+
4589
5379
  ## Environment Variables
4590
5380
 
4591
5381
  | Variable | Required | Description |
@@ -4597,7 +5387,8 @@ file defines a Starlette ASGI app with the chosen Agent framework SDK running wi
4597
5387
 
4598
5388
  If installation was successful, a virtual environment is already created with dependencies installed.
4599
5389
 
4600
- Run \`source .venv/bin/activate\` before developing.
5390
+ Activate the environment with \`source .venv/bin/activate\` on macOS/Linux, \`.venv\\Scripts\\activate.bat\` in Windows
5391
+ Command Prompt, or \`.\\.venv\\Scripts\\activate.ps1\` in Windows PowerShell.
4601
5392
 
4602
5393
  \`agentcore dev\` will start a local server on 0.0.0.0:8080.
4603
5394
 
@@ -4658,54 +5449,293 @@ Thumbs.db"
4658
5449
  `;
4659
5450
 
4660
5451
  exports[`Assets Directory Snapshots > Python framework assets > python/python/http/strands/base/main.py should match snapshot 1`] = `
4661
- "from strands import Agent, tool
5452
+ "from typing import Any
5453
+ from collections import OrderedDict
5454
+ {{#if inlineFunctionTools}}
5455
+ import json
5456
+
5457
+ from strands.tools.tools import PythonAgentTool
5458
+ from strands.types.tools import ToolResult, ToolUse
5459
+ {{/if}}
5460
+ from strands import Agent, tool
5461
+ {{#if hasSkillsFetcher}}
5462
+ from strands import AgentSkills
5463
+ {{#if hasFetchedSkills}}
5464
+ from skills.fetcher import resolve_s3_skills, resolve_git_skills
5465
+ {{/if}}
5466
+ {{#if (some gitSkills "credentialArn")}}
5467
+ from bedrock_agentcore.services.identity import IdentityClient
5468
+ {{/if}}
5469
+ {{/if}}
5470
+ import asyncio
5471
+ {{#if hasShell}}
5472
+ import subprocess
5473
+ {{/if}}
5474
+ {{#if hasFileOperations}}
5475
+ import os
5476
+ {{/if}}
5477
+ {{#if hasExecutionLimits}}
5478
+ from strands.tools.executors import SequentialToolExecutor
5479
+ from strands.types.exceptions import EventLoopException
5480
+ from hooks.execution_limits import ExecutionLimitExceeded, ExecutionLimitsHook
5481
+ {{/if}}
5482
+ {{#if hasConfigBundle}}
5483
+ from strands.hooks import HookProvider, HookRegistry, BeforeInvocationEvent, BeforeToolCallEvent
5484
+ {{/if}}
5485
+ {{#if truncationStrategy}}
5486
+ {{#if (eq truncationStrategy "sliding_window")}}
5487
+ from strands.agent.conversation_manager.sliding_window_conversation_manager import SlidingWindowConversationManager
5488
+ {{/if}}
5489
+ {{#if (eq truncationStrategy "summarization")}}
5490
+ from strands.agent.conversation_manager.summarizing_conversation_manager import SummarizingConversationManager
5491
+ {{/if}}
5492
+ {{else}}
5493
+ from strands.agent.conversation_manager.null_conversation_manager import NullConversationManager
5494
+ {{/if}}
5495
+ {{#if hasConfigBundle}}
5496
+ from bedrock_agentcore.runtime.context import BedrockAgentCoreContext
5497
+ {{/if}}
5498
+ {{#if hasBrowser}}
5499
+ from strands_tools.browser import AgentCoreBrowser
5500
+ {{/if}}
5501
+ {{#if hasCodeInterpreter}}
5502
+ from strands_tools.code_interpreter import AgentCoreCodeInterpreter
5503
+ {{/if}}
4662
5504
  from bedrock_agentcore.runtime import BedrockAgentCoreApp
4663
5505
  from model.load import load_model
4664
5506
  {{#if hasGateway}}
4665
5507
  from mcp_client.client import get_all_gateway_mcp_clients
4666
- {{else}}
4667
- from mcp_client.client import get_streamable_http_mcp_client
4668
5508
  {{/if}}
5509
+ {{#if remoteMcpTools}}
5510
+ from mcp_client.client import get_all_remote_mcp_clients
5511
+ {{/if}}
5512
+ {{#unless (or hasGateway remoteMcpTools)}}
5513
+ {{#unless isExportHarness}}
5514
+ from mcp_client.client import get_streamable_http_mcp_client
5515
+ {{/unless}}
5516
+ {{/unless}}
4669
5517
  {{#if hasMemory}}
4670
5518
  from memory.session import get_memory_session_manager
4671
5519
  {{/if}}
4672
- {{#if sessionStorageMountPath}}
5520
+ {{#unless hasFileOperations}}
5521
+ {{#if (or needsOs browserIdentifierEnvVar codeInterpreterIdentifierEnvVar (some gitSkills "credentialArn"))}}
4673
5522
  import os
4674
5523
  {{/if}}
5524
+ {{/unless}}
5525
+ {{#if hasPayment}}
5526
+ from capabilities.payments.payments import create_payments_plugin, PAYMENT_SYSTEM_PROMPT
5527
+ {{/if}}
4675
5528
 
4676
5529
  app = BedrockAgentCoreApp()
4677
5530
  log = app.logger
4678
5531
 
4679
- # Define a Streamable HTTP MCP Client
5532
+ {{#if (or hasGateway remoteMcpTools)}}
5533
+ # Define MCP clients for all configured MCP servers (gateways and/or remote MCP)
5534
+ mcp_clients = []
4680
5535
  {{#if hasGateway}}
4681
- mcp_clients = get_all_gateway_mcp_clients()
5536
+ mcp_clients += get_all_gateway_mcp_clients()
5537
+ {{/if}}
5538
+ {{#if remoteMcpTools}}
5539
+ mcp_clients += get_all_remote_mcp_clients()
5540
+ {{/if}}
4682
5541
  {{else}}
5542
+ {{#unless isExportHarness}}
5543
+ # Define a Streamable HTTP MCP Client
4683
5544
  mcp_clients = [get_streamable_http_mcp_client()]
5545
+ {{/unless}}
5546
+ {{/if}}
5547
+
5548
+ {{#if systemPromptText}}
5549
+ DEFAULT_SYSTEM_PROMPT = """{{escapePyStr systemPromptText}}"""
5550
+ {{else}}
5551
+ DEFAULT_SYSTEM_PROMPT = """
5552
+ You are a helpful assistant. Use tools when appropriate.
5553
+ {{#if needsOs}}{{#unless isExportHarness}}
5554
+ You have access to the following mounted filesystems. Use file_read, file_write, and list_files with full absolute paths:
5555
+ {{#if sessionStorageMountPath}}- {{sessionStorageMountPath}}: ephemeral session storage (lost when session ends)
5556
+ {{/if}}{{#each efsMounts}}- {{mountPath}}: EFS persistent storage (persists across sessions and agent restarts)
5557
+ {{/each}}{{#each s3Mounts}}- {{mountPath}}: S3 Files persistent storage (durable, backed by S3)
5558
+ {{/each}}{{/unless}}{{/if}}
5559
+ """
5560
+ {{/if}}
5561
+
5562
+ {{#if hasConfigBundle}}
5563
+ DEFAULT_TOOL_DESC = "Return the sum of two numbers"
4684
5564
  {{/if}}
4685
5565
 
4686
5566
  # Define a collection of tools used by the model
4687
5567
  tools = []
4688
5568
 
5569
+ {{#if inlineFunctionTools}}
5570
+ # Inline function tools — stop the agent loop so the tool call streams back to the caller
5571
+ def _make_inline_tool(name: str, spec: dict) -> PythonAgentTool:
5572
+ def _handler(tool: ToolUse, **kwargs: Any) -> ToolResult:
5573
+ kwargs.get("request_state", {})["stop_event_loop"] = True
5574
+ return {"toolUseId": tool["toolUseId"], "status": "success", "content": [{"text": " "}]}
5575
+ _handler.__name__ = name
5576
+ return PythonAgentTool(tool_name=name, tool_spec=spec, tool_func=_handler)
5577
+
5578
+ {{#each inlineFunctionTools}}
5579
+ _INLINE_SPEC_{{snakeCase name}} = {
5580
+ "name": "{{name}}",
5581
+ "description": {{safeJson description}},
5582
+ "inputSchema": {"json": json.loads({{pyJsonStr inputSchema}}) },
5583
+ }
5584
+ tools.append(_make_inline_tool("{{name}}", _INLINE_SPEC_{{snakeCase name}}))
5585
+ {{/each}}
5586
+
5587
+ _INLINE_FUNCTION_NAMES = { {{#each inlineFunctionTools}}"{{name}}"{{#unless @last}}, {{/unless}}{{/each}} }
5588
+
5589
+ {{else}}
5590
+ _INLINE_FUNCTION_NAMES = set()
5591
+
5592
+ {{#unless isExportHarness}}
4689
5593
  # Define a simple function tool
5594
+ {{#if hasConfigBundle}}
5595
+ @tool(description=DEFAULT_TOOL_DESC)
5596
+ {{else}}
4690
5597
  @tool
5598
+ {{/if}}
4691
5599
  def add_numbers(a: int, b: int) -> int:
4692
5600
  """Return the sum of two numbers"""
4693
5601
  return a+b
4694
5602
  tools.append(add_numbers)
4695
5603
 
4696
- {{#if sessionStorageMountPath}}
4697
- SESSION_STORAGE_PATH = "{{sessionStorageMountPath}}"
5604
+ {{/unless}}
5605
+ {{/if}}
5606
+ {{#if hasBrowser}}
5607
+ {{#if browserIdentifierEnvVar}}
5608
+ _browser_id = os.getenv("{{browserIdentifierEnvVar}}")
5609
+ tools.append(AgentCoreBrowser(**({"identifier": _browser_id} if _browser_id else {})).browser)
5610
+ {{else}}
5611
+ tools.append(AgentCoreBrowser().browser)
5612
+ {{/if}}
5613
+ {{/if}}
5614
+ {{#if hasCodeInterpreter}}
5615
+ {{#if codeInterpreterIdentifierEnvVar}}
5616
+ _code_interpreter_id = os.getenv("{{codeInterpreterIdentifierEnvVar}}")
5617
+ tools.append(AgentCoreCodeInterpreter(**({"identifier": _code_interpreter_id} if _code_interpreter_id else {})).code_interpreter)
5618
+ {{else}}
5619
+ tools.append(AgentCoreCodeInterpreter().code_interpreter)
5620
+ {{/if}}
5621
+ {{/if}}
5622
+ {{#if hasShell}}
5623
+ @tool
5624
+ def shell(command: str, timeout: int = 300) -> dict:
5625
+ """Execute a bash command and return the results.
5626
+
5627
+ Args:
5628
+ command: The bash command to execute
5629
+ timeout: Timeout in seconds (default: 300)
5630
+
5631
+ Returns:
5632
+ Dict with stdout, stderr, and exit_code
5633
+ """
5634
+ result = subprocess.run(
5635
+ command, shell=True, capture_output=True, text=True, timeout=timeout
5636
+ )
5637
+ return {"stdout": result.stdout, "stderr": result.stderr, "exit_code": result.returncode}
5638
+
5639
+ tools.append(shell)
5640
+ {{/if}}
5641
+ {{#if hasFileOperations}}
5642
+ @tool
5643
+ def file_operations(
5644
+ command: str,
5645
+ path: str,
5646
+ old_str: str = None,
5647
+ new_str: str = None,
5648
+ file_text: str = None,
5649
+ insert_line: int = None,
5650
+ view_range: list = None,
5651
+ ) -> str:
5652
+ """Text editor tool for viewing and modifying files.
5653
+
5654
+ Args:
5655
+ command: The command to execute ("view", "str_replace", "create", "insert")
5656
+ path: Path to the file or directory
5657
+ old_str: Text to replace (for str_replace command)
5658
+ new_str: Replacement text (for str_replace and insert commands)
5659
+ file_text: Content for new file (for create command)
5660
+ insert_line: Line number to insert after (for insert command)
5661
+ view_range: [start_line, end_line] for viewing specific lines (for view command)
5662
+
5663
+ Returns:
5664
+ Result of the operation
5665
+ """
5666
+ try:
5667
+ if command == "view":
5668
+ if not os.path.exists(path):
5669
+ return f"Error: Path '{path}' does not exist"
5670
+ if os.path.isdir(path):
5671
+ return "\\n".join(os.listdir(path))
5672
+ with open(path) as f:
5673
+ lines = f.read().splitlines()
5674
+ if view_range:
5675
+ start, end = view_range
5676
+ start_idx = max(0, start - 1)
5677
+ end_idx = len(lines) if end == -1 else min(len(lines), end)
5678
+ lines = lines[start_idx:end_idx]
5679
+ start_num = start_idx + 1
5680
+ else:
5681
+ start_num = 1
5682
+ return "\\n".join(f"{start_num + i}: {line}" for i, line in enumerate(lines))
5683
+ elif command == "str_replace":
5684
+ if old_str is None or new_str is None:
5685
+ return "Error: str_replace requires both old_str and new_str parameters"
5686
+ if not os.path.exists(path):
5687
+ return f"Error: File '{path}' does not exist"
5688
+ content = open(path).read()
5689
+ if old_str not in content:
5690
+ return "Error: Text not found in file"
5691
+ count = content.count(old_str)
5692
+ if count > 1:
5693
+ return f"Error: Text appears {count} times in file. Please be more specific."
5694
+ open(path, "w").write(content.replace(old_str, new_str, 1))
5695
+ return f"Successfully replaced text in '{path}'"
5696
+ elif command == "create":
5697
+ if file_text is None:
5698
+ return "Error: create requires file_text parameter"
5699
+ os.makedirs(os.path.dirname(os.path.abspath(path)), exist_ok=True)
5700
+ open(path, "w").write(file_text)
5701
+ return f"Successfully created file '{path}'"
5702
+ elif command == "insert":
5703
+ if new_str is None or insert_line is None:
5704
+ return "Error: insert requires both new_str and insert_line parameters"
5705
+ if not os.path.exists(path):
5706
+ return f"Error: File '{path}' does not exist"
5707
+ lines = open(path).read().splitlines(True)
5708
+ if insert_line == 0:
5709
+ lines.insert(0, new_str + "\\n")
5710
+ elif insert_line >= len(lines):
5711
+ lines.append(new_str + "\\n")
5712
+ else:
5713
+ lines.insert(insert_line, new_str + "\\n")
5714
+ open(path, "w").write("".join(lines))
5715
+ return f"Successfully inserted text in '{path}' at line {insert_line + 1}"
5716
+ else:
5717
+ return f"Error: Unknown command '{command}'"
5718
+ except Exception as e:
5719
+ return f"Error: {e}"
5720
+
5721
+ tools.append(file_operations)
5722
+ {{/if}}
5723
+ {{#if needsOs}}{{#unless isExportHarness}}
5724
+ _MOUNT_PATHS = [
5725
+ {{#if sessionStorageMountPath}}"{{sessionStorageMountPath}}",{{/if}}
5726
+ {{#each efsMounts}}"{{mountPath}}",{{/each}}
5727
+ {{#each s3Mounts}}"{{mountPath}}",{{/each}}
5728
+ ]
4698
5729
 
4699
5730
  def _safe_resolve(path: str) -> str:
4700
- """Resolve path safely within the storage boundary."""
4701
- resolved = os.path.realpath(os.path.join(SESSION_STORAGE_PATH, path.lstrip("/")))
4702
- if not resolved.startswith(os.path.realpath(SESSION_STORAGE_PATH)):
4703
- raise ValueError(f"Path '{path}' is outside the storage boundary")
5731
+ resolved = os.path.realpath(path)
5732
+ if not any(resolved == os.path.realpath(m) or resolved.startswith(os.path.realpath(m) + os.sep) for m in _MOUNT_PATHS):
5733
+ raise ValueError(f"Path '{path}' is not within any configured mount ({', '.join(_MOUNT_PATHS)})")
4704
5734
  return resolved
4705
5735
 
4706
5736
  @tool
4707
5737
  def file_read(path: str) -> str:
4708
- """Read a file from persistent storage. The path is relative to the storage root."""
5738
+ """Read a file from a mounted filesystem. Use the absolute path (e.g. /mnt/tools/data.txt)."""
4709
5739
  try:
4710
5740
  full_path = _safe_resolve(path)
4711
5741
  with open(full_path) as f:
@@ -4717,7 +5747,7 @@ def file_read(path: str) -> str:
4717
5747
 
4718
5748
  @tool
4719
5749
  def file_write(path: str, content: str) -> str:
4720
- """Write content to a file in persistent storage. The path is relative to the storage root."""
5750
+ """Write a file to a mounted filesystem. Use the absolute path (e.g. /mnt/tools/data.txt)."""
4721
5751
  try:
4722
5752
  full_path = _safe_resolve(path)
4723
5753
  parent = os.path.dirname(full_path)
@@ -4732,82 +5762,403 @@ def file_write(path: str, content: str) -> str:
4732
5762
  return f"Error writing '{path}': {e.strerror}"
4733
5763
 
4734
5764
  @tool
4735
- def list_files(directory: str = "") -> str:
4736
- """List files in persistent storage. The directory is relative to the storage root."""
5765
+ def list_files(path: str) -> str:
5766
+ """List files in a mounted filesystem directory. Use the absolute path (e.g. /mnt/tools)."""
4737
5767
  try:
4738
- target = _safe_resolve(directory)
4739
- entries = os.listdir(target)
5768
+ full_path = _safe_resolve(path)
5769
+ entries = os.listdir(full_path)
4740
5770
  return "\\n".join(entries) if entries else "(empty directory)"
4741
5771
  except ValueError as e:
4742
5772
  return str(e)
4743
5773
  except OSError as e:
4744
- return f"Error listing '{directory}': {e.strerror}"
5774
+ return f"Error listing '{path}': {e.strerror}"
4745
5775
 
4746
5776
  tools.extend([file_read, file_write, list_files])
4747
- {{/if}}
5777
+ {{/unless}}{{/if}}
4748
5778
 
5779
+ {{#if (or hasGateway remoteMcpTools)}}
5780
+ # Add MCP clients to tools
5781
+ for mcp_client in mcp_clients:
5782
+ if mcp_client:
5783
+ tools.append(mcp_client)
5784
+ {{else}}
5785
+ {{#unless isExportHarness}}
4749
5786
  # Add MCP client to tools if available
4750
5787
  for mcp_client in mcp_clients:
4751
5788
  if mcp_client:
4752
5789
  tools.append(mcp_client)
5790
+ {{/unless}}
5791
+ {{/if}}
5792
+
5793
+ {{#if hasConfigBundle}}
5794
+
5795
+ class ConfigBundleHook(HookProvider):
5796
+ """Injects config bundle values (system prompt, tool descriptions) before each invocation.
5797
+
5798
+ BedrockAgentCoreContext.get_config_bundle() fetches the component configuration
5799
+ for the current runtime ARN from the config bundle service. The SDK caches the
5800
+ result and refreshes on bundle version changes.
5801
+ """
5802
+
5803
+ def register_hooks(self, registry: HookRegistry, **kwargs: Any) -> None:
5804
+ registry.add_callback(BeforeInvocationEvent, self._inject_system_prompt)
5805
+ registry.add_callback(BeforeToolCallEvent, self._override_tool_desc)
5806
+
5807
+ def _inject_system_prompt(self, event: BeforeInvocationEvent) -> None:
5808
+ config = BedrockAgentCoreContext.get_config_bundle()
5809
+ prompt = config.get("systemPrompt", DEFAULT_SYSTEM_PROMPT)
5810
+
5811
+ if prompt != event.agent.system_prompt:
5812
+ event.agent.system_prompt = prompt
5813
+
5814
+ def _override_tool_desc(self, event: BeforeToolCallEvent) -> None:
5815
+ config = BedrockAgentCoreContext.get_config_bundle()
5816
+ tool_descs = config.get("toolDescriptions", {})
5817
+
5818
+ tool_name = event.tool_use["name"]
5819
+ override = tool_descs.get(tool_name)
5820
+ if override and event.selected_tool:
5821
+ spec = event.selected_tool.tool_spec
5822
+ if spec and "description" in spec:
5823
+ spec["description"] = override
4753
5824
 
4754
- SYSTEM_PROMPT = """
4755
- You are a helpful assistant. Use tools when appropriate.
4756
- {{#if sessionStorageMountPath}}
4757
- You have persistent storage at {{sessionStorageMountPath}}. Use file tools to read and write files. Data persists across sessions.
4758
5825
  {{/if}}
4759
- """
5826
+
5827
+ def _make_conversation_manager():
5828
+ {{#if truncationStrategy}}
5829
+ {{#if (eq truncationStrategy "sliding_window")}}
5830
+ {{#if truncationConfig}}
5831
+ return SlidingWindowConversationManager(**{{safeJson truncationConfig}}, per_turn=True)
5832
+ {{else}}
5833
+ return SlidingWindowConversationManager(per_turn=True)
5834
+ {{/if}}
5835
+ {{else}}
5836
+ {{#if truncationConfig}}
5837
+ return SummarizingConversationManager(**{{safeJson truncationConfig}})
5838
+ {{else}}
5839
+ return SummarizingConversationManager()
5840
+ {{/if}}
5841
+ {{/if}}
5842
+ {{else}}
5843
+ return NullConversationManager()
5844
+ {{/if}}
4760
5845
 
4761
5846
  {{#if hasMemory}}
5847
+ {{#unless hasPayment}}
4762
5848
  def agent_factory():
4763
5849
  cache = {}
4764
- def get_or_create_agent(session_id, user_id):
4765
- key = f"{session_id}/{user_id}"
5850
+ def get_or_create_agent(session_id, user_id{{#if hasSkillsFetcher}}, skill_plugins=None{{/if}}):
5851
+ {{#if actorId}}
5852
+ _actor_id = "{{actorId}}"
5853
+ {{else}}
5854
+ _actor_id = user_id
5855
+ {{/if}}
5856
+ key = f"{session_id}/{_actor_id}"
4766
5857
  if key not in cache:
4767
- # Create an agent for the given session_id and user_id
4768
5858
  cache[key] = Agent(
4769
5859
  model=load_model(),
4770
- session_manager=get_memory_session_manager(session_id, user_id),
4771
- system_prompt=SYSTEM_PROMPT,
4772
- tools=tools
5860
+ session_manager=get_memory_session_manager(session_id, _actor_id),
5861
+ conversation_manager=_make_conversation_manager(),
5862
+ system_prompt=DEFAULT_SYSTEM_PROMPT,
5863
+ tools=tools,
5864
+ {{#if hasSkillsFetcher}}
5865
+ plugins=skill_plugins or None,
5866
+ {{/if}}
5867
+ {{#if hasExecutionLimits}}
5868
+ tool_executor=SequentialToolExecutor(),
5869
+ callback_handler=None,
5870
+ {{/if}}
5871
+ hooks=[
5872
+ {{#if hasExecutionLimits}}
5873
+ ExecutionLimitsHook(
5874
+ {{#if maxIterations}}max_iterations={{maxIterations}},{{/if}}
5875
+ {{#if maxTokens}}max_tokens={{maxTokens}},{{/if}}
5876
+ {{#if timeoutSeconds}}timeout_seconds={{timeoutSeconds}},{{/if}}
5877
+ ),
5878
+ {{/if}}
5879
+ {{#if hasConfigBundle}}
5880
+ ConfigBundleHook(),
5881
+ {{/if}}
5882
+ ],
4773
5883
  )
4774
5884
  return cache[key]
4775
5885
  return get_or_create_agent
4776
5886
  get_or_create_agent = agent_factory()
5887
+ {{/unless}}
4777
5888
  {{else}}
4778
- _agent = None
4779
-
4780
- def get_or_create_agent():
4781
- global _agent
4782
- if _agent is None:
4783
- _agent = Agent(
5889
+ {{#unless hasPayment}}
5890
+ # Reuses one Agent per session_id so each session keeps its own in-process
5891
+ # conversation history (best-effort; resets on cold start). The cache is bounded
5892
+ # to 128 sessions with LRU eviction (least-recently-used is dropped and its
5893
+ # history reset) so a single process serving many sessions cannot leak history
5894
+ # between them or grow without limit. For durable history, attach a session manager.
5895
+ def agent_factory():
5896
+ cache = OrderedDict()
5897
+ def get_or_create_agent(session_id{{#if hasSkillsFetcher}}, skill_plugins=None{{/if}}):
5898
+ if session_id in cache:
5899
+ cache.move_to_end(session_id)
5900
+ return cache[session_id]
5901
+ if len(cache) >= 128:
5902
+ cache.popitem(last=False)
5903
+ cache[session_id] = Agent(
4784
5904
  model=load_model(),
4785
- system_prompt=SYSTEM_PROMPT,
4786
- tools=tools
5905
+ system_prompt=DEFAULT_SYSTEM_PROMPT,
5906
+ tools=tools,
5907
+ conversation_manager=_make_conversation_manager(),
5908
+ {{#if hasSkillsFetcher}}
5909
+ plugins=skill_plugins or None,
5910
+ {{/if}}
5911
+ {{#if hasExecutionLimits}}
5912
+ tool_executor=SequentialToolExecutor(),
5913
+ callback_handler=None,
5914
+ {{/if}}
5915
+ hooks=[
5916
+ {{#if hasExecutionLimits}}
5917
+ ExecutionLimitsHook(
5918
+ {{#if maxIterations}}max_iterations={{maxIterations}},{{/if}}
5919
+ {{#if maxTokens}}max_tokens={{maxTokens}},{{/if}}
5920
+ {{#if timeoutSeconds}}timeout_seconds={{timeoutSeconds}},{{/if}}
5921
+ ),
5922
+ {{/if}}
5923
+ {{#if hasConfigBundle}}
5924
+ ConfigBundleHook(),
5925
+ {{/if}}
5926
+ ],
4787
5927
  )
4788
- return _agent
5928
+ return cache[session_id]
5929
+ return get_or_create_agent
5930
+ get_or_create_agent = agent_factory()
5931
+ {{/unless}}
4789
5932
  {{/if}}
4790
5933
 
4791
5934
 
5935
+ def strip_trailing_tool_use(messages: Any) -> list[dict]:
5936
+ """Strip toolUse blocks from the tail until the last message has none."""
5937
+ if not isinstance(messages, list):
5938
+ raise ValueError("messages must be a list")
5939
+
5940
+ messages = list(messages)
5941
+ while messages:
5942
+ last = messages[-1]
5943
+ if not isinstance(last, dict):
5944
+ raise ValueError("each message must be an object")
5945
+ original_content = last.get("content", [])
5946
+ if not isinstance(original_content, list) or not all(isinstance(block, dict) for block in original_content):
5947
+ raise ValueError("each message content value must be a list of content blocks")
5948
+
5949
+ content = [block for block in original_content if "toolUse" not in block]
5950
+ if len(content) == len(original_content):
5951
+ break
5952
+ if content:
5953
+ messages[-1] = {**last, "content": content}
5954
+ break
5955
+ messages.pop()
5956
+
5957
+ return messages
5958
+
5959
+
5960
+ def _extract_prompt(payload: dict):
5961
+ """Accept validated harness messages, tool results, or a plain prompt string."""
5962
+ if not isinstance(payload, dict):
5963
+ raise ValueError("payload must be a JSON object")
5964
+ if "messages" in payload:
5965
+ return strip_trailing_tool_use(payload["messages"])
5966
+ if "tool_results" in payload:
5967
+ tool_results = payload["tool_results"]
5968
+ if not isinstance(tool_results, list) or not all(
5969
+ isinstance(tool_result, dict) and isinstance(tool_result.get("toolUseId"), str)
5970
+ for tool_result in tool_results
5971
+ ):
5972
+ raise ValueError("tool_results must contain objects with a toolUseId string")
5973
+ return [{"role": "user", "content": [{"toolResult": {
5974
+ "toolUseId": tr["toolUseId"],
5975
+ "status": tr.get("status", "success"),
5976
+ "content": tr.get("content", []),
5977
+ }} for tr in tool_results]}]
5978
+ prompt = payload.get("prompt", "")
5979
+ if not isinstance(prompt, str):
5980
+ raise ValueError("prompt must be a string")
5981
+ return prompt
5982
+
5983
+
5984
+ def _has_inline_function_call(messages) -> bool:
5985
+ """Return True if messages contains an assistant toolUse for an inline function tool."""
5986
+ if not _INLINE_FUNCTION_NAMES or not isinstance(messages, list):
5987
+ return False
5988
+ for msg in messages:
5989
+ if msg.get("role") == "assistant":
5990
+ for block in msg.get("content", []):
5991
+ if isinstance(block, dict) and block.get("toolUse", {}).get("name") in _INLINE_FUNCTION_NAMES:
5992
+ return True
5993
+ return False
5994
+
5995
+
5996
+ def _is_inline_function_call(event: dict) -> bool:
5997
+ """Check if a contentBlockStart event is for an inline function tool."""
5998
+ if not _INLINE_FUNCTION_NAMES:
5999
+ return False
6000
+ cbs = event.get("contentBlockStart", {})
6001
+ start = cbs.get("start", {})
6002
+ tool_use = start.get("toolUse") if isinstance(start, dict) else None
6003
+ return tool_use is not None and tool_use.get("name") in _INLINE_FUNCTION_NAMES
6004
+
6005
+
6006
+
4792
6007
  @app.entrypoint
4793
6008
  async def invoke(payload, context):
4794
6009
  log.info("Invoking Agent.....")
4795
6010
 
6011
+ {{#if hasPayment}}
6012
+ user_id = payload.get("user_id") or getattr(context, "user_id", "default-user")
6013
+ instrument_id = payload.get("payment_instrument_id")
6014
+ session_id = payload.get("payment_session_id")
6015
+ payments_plugin = create_payments_plugin(user_id, instrument_id, session_id)
6016
+ plugins = [payments_plugin] if payments_plugin else []
6017
+ {{/if}}
6018
+ {{#if hasSkillsFetcher}}
6019
+ skill_paths = [{{#each pathSkills}}{{safeJson this}}{{#unless @last}}, {{/unless}}{{/each}}]
6020
+ {{#if s3Skills}}
6021
+ s3_skill_sources = [{{#each s3Skills}}{{safeJson this}}{{#unless @last}}, {{/unless}}{{/each}}]
6022
+ skill_paths.extend(await asyncio.to_thread(resolve_s3_skills, s3_skill_sources, None))
6023
+ {{/if}}
6024
+ {{#if gitSkills}}
6025
+ git_skill_sources = [
6026
+ {{#each gitSkills}}
6027
+ 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}}),
6028
+ {{/each}}
6029
+ ]
6030
+ {{#if (some gitSkills "credentialArn")}}
6031
+ _git_identity_client = IdentityClient(os.environ.get("AWS_REGION", os.environ.get("AWS_DEFAULT_REGION", "us-east-1")))
6032
+ {{else}}
6033
+ _git_identity_client = None
6034
+ {{/if}}
6035
+ skill_paths.extend(await asyncio.to_thread(resolve_git_skills, git_skill_sources, _git_identity_client))
6036
+ {{/if}}
6037
+ _skill_plugins = [AgentSkills(skills=skill_paths)] if skill_paths else []
6038
+ {{/if}}
6039
+
4796
6040
  {{#if hasMemory}}
6041
+ {{#if hasPayment}}
6042
+ mem_session_id = getattr(context, 'session_id', 'default-session')
6043
+ {{#if actorId}}
6044
+ mem_user_id = "{{actorId}}"
6045
+ {{else}}
6046
+ mem_user_id = getattr(context, 'user_id', 'default-user')
6047
+ {{/if}}
6048
+ agent = Agent(
6049
+ model=load_model(),
6050
+ session_manager=get_memory_session_manager(mem_session_id, mem_user_id),
6051
+ system_prompt=DEFAULT_SYSTEM_PROMPT + PAYMENT_SYSTEM_PROMPT,
6052
+ tools=tools,
6053
+ plugins=plugins{{#if hasSkillsFetcher}} + _skill_plugins{{/if}},{{#if hasConfigBundle}}
6054
+ hooks=[ConfigBundleHook()],{{/if}}
6055
+ )
6056
+ {{else}}
4797
6057
  session_id = getattr(context, 'session_id', 'default-session')
6058
+ {{#if actorId}}
6059
+ user_id = "{{actorId}}"
6060
+ {{else}}
4798
6061
  user_id = getattr(context, 'user_id', 'default-user')
4799
- agent = get_or_create_agent(session_id, user_id)
6062
+ {{/if}}
6063
+ agent = get_or_create_agent(session_id, user_id{{#if hasSkillsFetcher}}, _skill_plugins{{/if}})
6064
+ {{/if}}
6065
+ {{else}}
6066
+ {{#if hasPayment}}
6067
+ agent = Agent(
6068
+ model=load_model(),
6069
+ system_prompt=DEFAULT_SYSTEM_PROMPT + PAYMENT_SYSTEM_PROMPT,
6070
+ tools=tools,
6071
+ plugins=plugins{{#if hasSkillsFetcher}} + _skill_plugins{{/if}},{{#if hasConfigBundle}}
6072
+ hooks=[ConfigBundleHook()],{{/if}}
6073
+ )
4800
6074
  {{else}}
4801
- agent = get_or_create_agent()
6075
+ session_id = getattr(context, 'session_id', 'default-session')
6076
+ agent = get_or_create_agent(session_id{{#if hasSkillsFetcher}}, _skill_plugins{{/if}})
6077
+ {{/if}}
4802
6078
  {{/if}}
4803
6079
 
4804
- # Execute and format response
4805
- stream = agent.stream_async(payload.get("prompt"))
6080
+ prompt = _extract_prompt(payload)
6081
+
6082
+ {{#if inlineFunctionTools}}
6083
+ # If Turn 2 carries the harness-style assistant(toolUse)+user(toolResult) pair,
6084
+ # strip the placeholder turn Strands stored during Turn 1 so the real toolResult
6085
+ # is injected cleanly — same protocol as the harness runtime.
6086
+ if _has_inline_function_call(prompt):
6087
+ msgs = agent.messages
6088
+ if len(msgs) >= 2 and any("toolResult" in b for b in msgs[-1].get("content", [])):
6089
+ del msgs[-2:]
6090
+ {{/if}}
6091
+
6092
+ {{#if hasExecutionLimits}}
6093
+ timeout_seconds = {{#if timeoutSeconds}}{{timeoutSeconds}}{{else}}None{{/if}}
6094
+ timeout_fired = False
6095
+ watchdog_task = None
6096
+ if timeout_seconds is not None:
6097
+ async def _timeout_watchdog():
6098
+ nonlocal timeout_fired
6099
+ await asyncio.sleep(timeout_seconds)
6100
+ timeout_fired = True
6101
+ agent.cancel()
6102
+ watchdog_task = asyncio.create_task(_timeout_watchdog())
4806
6103
 
4807
- async for event in stream:
4808
- # Handle Text parts of the response
4809
- if "data" in event and isinstance(event["data"], str):
4810
- yield event["data"]
6104
+ try:
6105
+ {{#if inlineFunctionTools}}
6106
+ hit_inline_function = False
6107
+ {{/if}}
6108
+ async for event in agent.stream_async(
6109
+ prompt,
6110
+ ):
6111
+ if not isinstance(event, dict) or "event" not in event:
6112
+ continue
6113
+ cbs = event["event"].get("contentBlockStart")
6114
+ if cbs is not None and not cbs.get("start"):
6115
+ continue
6116
+ {{#if inlineFunctionTools}}
6117
+ if not hit_inline_function:
6118
+ hit_inline_function = _is_inline_function_call(event["event"])
6119
+ {{/if}}
6120
+ yield event
6121
+ {{#if inlineFunctionTools}}
6122
+ if hit_inline_function and "messageStop" in event["event"]:
6123
+ return
6124
+ {{/if}}
6125
+
6126
+ if timeout_fired:
6127
+ yield {"event": {"messageStop": {"stopReason": "timeout_exceeded"}}}
6128
+ except EventLoopException as e:
6129
+ if isinstance(e.original_exception, ExecutionLimitExceeded):
6130
+ yield {"event": {"messageStop": {"stopReason": str(e.original_exception)}}}
6131
+ return
6132
+ raise
6133
+ finally:
6134
+ if watchdog_task is not None:
6135
+ watchdog_task.cancel()
6136
+ try:
6137
+ await watchdog_task
6138
+ except asyncio.CancelledError:
6139
+ pass
6140
+ {{else}}
6141
+ {{#if inlineFunctionTools}}
6142
+ hit_inline_function = False
6143
+ {{/if}}
6144
+ async for event in agent.stream_async(
6145
+ prompt,
6146
+ ):
6147
+ if not isinstance(event, dict) or "event" not in event:
6148
+ continue
6149
+ cbs = event["event"].get("contentBlockStart")
6150
+ if cbs is not None and not cbs.get("start"):
6151
+ continue
6152
+ {{#if inlineFunctionTools}}
6153
+ if not hit_inline_function:
6154
+ hit_inline_function = _is_inline_function_call(event["event"])
6155
+ {{/if}}
6156
+ yield event
6157
+ {{#if inlineFunctionTools}}
6158
+ if hit_inline_function and "messageStop" in event["event"]:
6159
+ return
6160
+ {{/if}}
6161
+ {{/if}}
4811
6162
 
4812
6163
 
4813
6164
  if __name__ == "__main__":
@@ -4841,7 +6192,10 @@ from bedrock_agentcore.identity import requires_access_token
4841
6192
  @requires_access_token(
4842
6193
  provider_name="{{credentialProviderName}}",
4843
6194
  scopes=[{{#if scopes}}"{{scopes}}"{{/if}}],
4844
- auth_flow="M2M",
6195
+ auth_flow="{{#if authFlow}}{{authFlow}}{{else}}M2M{{/if}}",
6196
+ {{#if customParameters}}
6197
+ custom_parameters={{safeJson customParameters}},
6198
+ {{/if}}
4845
6199
  )
4846
6200
  def _get_bearer_token_{{snakeCase name}}(*, access_token: str):
4847
6201
  """Obtain OAuth access token via AgentCore Identity for {{name}}."""
@@ -4852,18 +6206,22 @@ def _get_bearer_token_{{snakeCase name}}(*, access_token: str):
4852
6206
  {{#each gatewayProviders}}
4853
6207
  def get_{{snakeCase name}}_mcp_client() -> MCPClient | None:
4854
6208
  """Returns an MCP Client connected to the {{name}} gateway."""
6209
+ {{#if hardcodedUrl}}
6210
+ url = {{safeJson hardcodedUrl}}
6211
+ {{else}}
4855
6212
  url = os.environ.get("{{envVarName}}")
4856
6213
  if not url:
4857
6214
  logger.warning("{{envVarName}} not set — {{name}} gateway tools unavailable")
4858
6215
  return None
6216
+ {{/if}}
4859
6217
  {{#if (eq authType "AWS_IAM")}}
4860
- 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"))))
6218
+ 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}}")
4861
6219
  {{else if (eq authType "CUSTOM_JWT")}}
4862
6220
  token = _get_bearer_token_{{snakeCase name}}()
4863
6221
  headers = {"Authorization": f"Bearer {token}"} if token else {}
4864
- return MCPClient(lambda: streamablehttp_client(url, headers=headers))
6222
+ return MCPClient(lambda: streamablehttp_client(url, headers=headers), prefix="{{snakeCase name}}")
4865
6223
  {{else}}
4866
- return MCPClient(lambda: streamablehttp_client(url))
6224
+ return MCPClient(lambda: streamablehttp_client(url), prefix="{{snakeCase name}}")
4867
6225
  {{/if}}
4868
6226
 
4869
6227
  {{/each}}
@@ -4876,7 +6234,41 @@ def get_all_gateway_mcp_clients() -> list[MCPClient]:
4876
6234
  clients.append(client)
4877
6235
  {{/each}}
4878
6236
  return clients
4879
- {{else}}
6237
+ {{/if}}
6238
+ {{#if remoteMcpTools}}
6239
+ {{#if (some remoteMcpTools "headerCredentials")}}
6240
+ from bedrock_agentcore.identity.auth import requires_api_key
6241
+ {{/if}}
6242
+ {{#each remoteMcpTools}}
6243
+ {{#if headerCredentials}}
6244
+ {{#each headerCredentials}}
6245
+ @requires_api_key(provider_name="{{credentialName}}")
6246
+ def _get_{{snakeCase ../name}}_{{snakeCase headerKey}}_key(api_key: str) -> str:
6247
+ """Fetch {{headerKey}} credential for {{../name}} from AgentCore Identity."""
6248
+ return api_key
6249
+
6250
+ {{/each}}
6251
+ {{/if}}
6252
+ def get_{{snakeCase name}}_mcp_client() -> MCPClient | None:
6253
+ """Returns an MCP Client for the {{name}} remote MCP server."""
6254
+ url = {{safeJson url}}
6255
+ {{#if headerCredentials}}
6256
+ if os.getenv("LOCAL_DEV") == "1":
6257
+ headers = { {{#each headerCredentials}}{{safeJson headerKey}}: os.environ.get("{{envVarName}}", ""){{#unless @last}}, {{/unless}}{{/each}} }
6258
+ else:
6259
+ headers = { {{#each headerCredentials}}{{safeJson headerKey}}: _get_{{snakeCase ../name}}_{{snakeCase headerKey}}_key(){{#unless @last}}, {{/unless}}{{/each}} }
6260
+ return MCPClient(lambda: streamablehttp_client(url, headers=headers))
6261
+ {{else}}
6262
+ return MCPClient(lambda: streamablehttp_client(url))
6263
+ {{/if}}
6264
+
6265
+ {{/each}}
6266
+ def get_all_remote_mcp_clients() -> list[MCPClient]:
6267
+ """Returns all configured remote MCP clients."""
6268
+ clients = [{{#each remoteMcpTools}}get_{{snakeCase name}}_mcp_client(){{#unless @last}}, {{/unless}}{{/each}}]
6269
+ return [c for c in clients if c is not None]
6270
+ {{/if}}
6271
+ {{#unless (or hasGateway remoteMcpTools)}}
4880
6272
  {{#if isVpc}}
4881
6273
  # VPC mode: external MCP endpoints are not reachable without a NAT gateway.
4882
6274
  # Add an AgentCore Gateway with \`agentcore add gateway\`, or configure your own endpoint below.
@@ -4885,6 +6277,7 @@ def get_streamable_http_mcp_client() -> MCPClient | None:
4885
6277
  """No MCP server configured. Add a gateway with \`agentcore add gateway\`."""
4886
6278
  return None
4887
6279
  {{else}}
6280
+ {{#unless isExportHarness}}
4888
6281
  # ExaAI provides information about code through web searches, crawling and code context searches through their platform. Requires no authentication
4889
6282
  EXAMPLE_MCP_ENDPOINT = "https://mcp.exa.ai/mcp"
4890
6283
 
@@ -4892,8 +6285,9 @@ def get_streamable_http_mcp_client() -> MCPClient:
4892
6285
  """Returns an MCP Client compatible with Strands"""
4893
6286
  # to use an MCP server that supports bearer authentication, add headers={"Authorization": f"Bearer {access_token}"}
4894
6287
  return MCPClient(lambda: streamablehttp_client(EXAMPLE_MCP_ENDPOINT))
6288
+ {{/unless}}
4895
6289
  {{/if}}
4896
- {{/if}}
6290
+ {{/unless}}
4897
6291
  "
4898
6292
  `;
4899
6293
 
@@ -4904,12 +6298,74 @@ exports[`Assets Directory Snapshots > Python framework assets > python/python/ht
4904
6298
 
4905
6299
  exports[`Assets Directory Snapshots > Python framework assets > python/python/http/strands/base/model/load.py should match snapshot 1`] = `
4906
6300
  "{{#if (eq modelProvider "Bedrock")}}
6301
+ {{#if bedrockMantle}}
6302
+ import os
6303
+
6304
+ from aws_bedrock_token_generator import provide_token
6305
+ {{#if (eq mantleApiFormat "chat_completions")}}
6306
+ from strands.models.openai import OpenAIModel
6307
+ {{else}}
6308
+ {{#if mantleProprietary}}
6309
+ from strands.models.openai_responses import OpenAIResponsesModel
6310
+ {{else}}
6311
+ from model.mantle_compat import MantleCompatResponsesModel
6312
+ {{/if}}
6313
+ {{/if}}
6314
+
6315
+ MODEL_ID = "{{modelId}}"
6316
+
6317
+
6318
+ def load_model():
6319
+ """
6320
+ Get a Bedrock Mantle model client. These OpenAI-compatible models (e.g. openai.gpt-5.5,
6321
+ openai.gpt-oss-120b) are served via the Bedrock Mantle endpoint, NOT the Converse API — so they
6322
+ are invoked through an OpenAI-style client authenticated with a short-lived Bedrock bearer token.
6323
+ Region is read from AWS_REGION (set by the AgentCore runtime).
6324
+ """
6325
+ region = os.environ.get("AWS_REGION", os.environ.get("AWS_DEFAULT_REGION", "us-east-1"))
6326
+ token = provide_token(region=region)
6327
+ {{#if mantleProprietary}}
6328
+ # Proprietary OpenAI models only work on the /openai/v1 Mantle path.
6329
+ base_url = f"https://bedrock-mantle.{region}.api.aws/openai/v1"
6330
+ {{else}}
6331
+ # Open-source OpenAI models (gpt-oss-*) only work on the /v1 Mantle path.
6332
+ base_url = f"https://bedrock-mantle.{region}.api.aws/v1"
6333
+ {{/if}}
6334
+ client_args = {"api_key": token, "base_url": base_url}
6335
+
6336
+ params = {}
6337
+ {{#if modelMaxTokens}}
6338
+ {{#if (eq mantleApiFormat "chat_completions")}}
6339
+ params["max_completion_tokens"] = {{modelMaxTokens}}
6340
+ {{else}}
6341
+ params["max_output_tokens"] = {{modelMaxTokens}}
6342
+ {{/if}}
6343
+ {{/if}}
6344
+ {{#if modelTemperature}}
6345
+ params["temperature"] = {{modelTemperature}}
6346
+ {{/if}}
6347
+ {{#if modelTopP}}
6348
+ params["top_p"] = {{modelTopP}}
6349
+ {{/if}}
6350
+ {{#if (eq mantleApiFormat "chat_completions")}}
6351
+ return OpenAIModel(client_args=client_args, model_id=MODEL_ID, params=params)
6352
+ {{else}}
6353
+ # Responses API: Mantle does not persist responses, so disable server-side storage.
6354
+ params["store"] = False
6355
+ {{#if mantleProprietary}}
6356
+ return OpenAIResponsesModel(client_args=client_args, model_id=MODEL_ID, params=params)
6357
+ {{else}}
6358
+ return MantleCompatResponsesModel(client_args=client_args, model_id=MODEL_ID, params=params)
6359
+ {{/if}}
6360
+ {{/if}}
6361
+ {{else}}
4907
6362
  from strands.models.bedrock import BedrockModel
4908
6363
 
4909
6364
 
4910
6365
  def load_model() -> BedrockModel:
4911
6366
  """Get Bedrock model client using IAM credentials."""
4912
- return BedrockModel(model_id="global.anthropic.claude-sonnet-4-5-20250929-v1:0")
6367
+ return BedrockModel(model_id="{{#if modelId}}{{modelId}}{{else}}global.anthropic.claude-sonnet-4-5-20250929-v1:0{{/if}}"{{#if modelMaxTokens}}, max_tokens={{modelMaxTokens}}{{/if}})
6368
+ {{/if}}
4913
6369
  {{/if}}
4914
6370
  {{#if (eq modelProvider "Anthropic")}}
4915
6371
  import os
@@ -4985,7 +6441,7 @@ def load_model() -> OpenAIModel:
4985
6441
  """Get authenticated OpenAI model client."""
4986
6442
  return OpenAIModel(
4987
6443
  client_args={"api_key": _get_api_key()},
4988
- model_id="gpt-4.1",
6444
+ model_id="{{#if modelId}}{{modelId}}{{else}}gpt-4.1{{/if}}",
4989
6445
  )
4990
6446
  {{/if}}
4991
6447
  {{#if (eq modelProvider "Gemini")}}
@@ -5023,10 +6479,89 @@ def load_model() -> GeminiModel:
5023
6479
  """Get authenticated Gemini model client."""
5024
6480
  return GeminiModel(
5025
6481
  client_args={"api_key": _get_api_key()},
5026
- model_id="gemini-2.5-flash",
6482
+ model_id="{{#if modelId}}{{modelId}}{{else}}gemini-2.5-flash{{/if}}",
5027
6483
  )
5028
6484
  {{/if}}
5029
- "
6485
+ {{#if (eq modelProvider "LiteLLM")}}
6486
+ import os
6487
+ {{#if litellmAdditionalParams}}
6488
+ import json
6489
+ {{/if}}
6490
+
6491
+ from strands.models.litellm import LiteLLMModel
6492
+ {{#if identityProviders.[0].name}}
6493
+ from bedrock_agentcore.identity.auth import requires_api_key
6494
+
6495
+ IDENTITY_PROVIDER_NAME = "{{identityProviders.[0].name}}"
6496
+ IDENTITY_ENV_VAR = "{{identityProviders.[0].envVarName}}"
6497
+
6498
+
6499
+ @requires_api_key(provider_name=IDENTITY_PROVIDER_NAME)
6500
+ def _agentcore_identity_api_key_provider(api_key: str) -> str:
6501
+ """Fetch API key from AgentCore Identity."""
6502
+ return api_key
6503
+
6504
+
6505
+ def _get_api_key() -> str:
6506
+ """
6507
+ Uses AgentCore Identity for API key management in deployed environments.
6508
+ For local development, run via 'agentcore dev' which loads agentcore/.env.
6509
+ """
6510
+ if os.getenv("LOCAL_DEV") == "1":
6511
+ api_key = os.getenv(IDENTITY_ENV_VAR)
6512
+ if not api_key:
6513
+ raise RuntimeError(
6514
+ f"{IDENTITY_ENV_VAR} not found. Add {IDENTITY_ENV_VAR}=your-key to .env.local"
6515
+ )
6516
+ return api_key
6517
+ return _agentcore_identity_api_key_provider()
6518
+ {{/if}}
6519
+
6520
+
6521
+
6522
+
6523
+ def load_model() -> LiteLLMModel:
6524
+ """Get a LiteLLM model client (proxies to the provider encoded in model_id)."""
6525
+ client_args = {}
6526
+ {{#if identityProviders.[0].name}}
6527
+ client_args["api_key"] = _get_api_key()
6528
+ {{/if}}
6529
+ {{#if litellmApiBase}}
6530
+ client_args["api_base"] = {{safeJson litellmApiBase}}
6531
+ {{/if}}
6532
+ params = {{#if litellmAdditionalParams}}json.loads({{pyJsonStr litellmAdditionalParams}}){{else}}{}{{/if}}
6533
+ return LiteLLMModel(
6534
+ client_args=client_args,
6535
+ model_id="{{#if modelId}}{{modelId}}{{else}}bedrock/us.anthropic.claude-sonnet-4-5-20250514-v1:0{{/if}}",
6536
+ params=params,
6537
+ )
6538
+ {{/if}}
6539
+ "
6540
+ `;
6541
+
6542
+ exports[`Assets Directory Snapshots > Python framework assets > python/python/http/strands/base/model/mantle_compat.py should match snapshot 1`] = `
6543
+ "from strands.models.openai_responses import OpenAIResponsesModel
6544
+
6545
+
6546
+ class MantleCompatResponsesModel(OpenAIResponsesModel):
6547
+ """Workaround for Bedrock Mantle rejecting output_text in EasyInputMessage content arrays.
6548
+
6549
+ Mantle's Pydantic validation only accepts content as a plain string for assistant messages, while
6550
+ real OpenAI accepts both formats. Flatten assistant content arrays to strings so multi-turn works.
6551
+ Used for open-source OpenAI models (gpt-oss-*) on the /v1 Mantle path; proprietary models use the
6552
+ plain OpenAIResponsesModel on /openai/v1.
6553
+ """
6554
+
6555
+ @classmethod
6556
+ def _format_request_messages(cls, messages):
6557
+ formatted = super()._format_request_messages(messages)
6558
+ for msg in formatted:
6559
+ if msg.get("role") == "assistant" and isinstance(msg.get("content"), list):
6560
+ msg["content"] = "".join(
6561
+ part.get("text", "") for part in msg["content"] if part.get("type") == "output_text"
6562
+ )
6563
+ return formatted
6564
+ "
5030
6565
  `;
5031
6566
 
5032
6567
  exports[`Assets Directory Snapshots > Python framework assets > python/python/http/strands/base/pyproject.toml should match snapshot 1`] = `
@@ -5042,14 +6577,20 @@ readme = "README.md"
5042
6577
  requires-python = ">=3.10"
5043
6578
  dependencies = [
5044
6579
  {{#if (eq modelProvider "Anthropic")}}"anthropic >= 0.30.0",
5045
- {{/if}}"aws-opentelemetry-distro",
5046
- "bedrock-agentcore >= 1.0.3",
6580
+ {{/if}}"aws-opentelemetry-distro ~= 0.18.0",
6581
+ "bedrock-agentcore >= 1.9.1",
5047
6582
  "botocore[crt] >= 1.35.0",
5048
6583
  {{#if (eq modelProvider "Gemini")}}"google-genai >= 1.0.0",
5049
- {{/if}}"mcp >= 1.19.0",
6584
+ {{/if}}"mcp ~= 1.24.0",
5050
6585
  {{#if (eq modelProvider "OpenAI")}}"openai >= 1.0.0",
5051
- {{/if}}"strands-agents >= 1.13.0",
5052
- {{#if hasGateway}}{{#if (includes gatewayAuthTypes "AWS_IAM")}}"mcp-proxy-for-aws >= 1.1.0",
6586
+ {{/if}}{{#if (eq modelProvider "LiteLLM")}}"litellm >= 1.0.0",
6587
+ {{/if}}{{#if bedrockMantle}}"openai >= 1.0.0",
6588
+ "aws-bedrock-token-generator >= 1.0.0",
6589
+ {{/if}}"strands-agents >= 1.15.0",
6590
+ {{#if (or hasBrowser hasCodeInterpreter)}}"strands-agents-tools >= 0.1.0",
6591
+ {{/if}}{{#if hasBrowser}}"nest-asyncio >= 1.5.0",
6592
+ "playwright >= 1.42.0",
6593
+ {{/if}}{{#if hasGateway}}{{#if (includes gatewayAuthTypes "AWS_IAM")}}"mcp-proxy-for-aws >= 1.1.0",
5053
6594
  {{/if}}{{/if}}
5054
6595
  ]
5055
6596
 
@@ -5058,6 +6599,347 @@ packages = ["."]
5058
6599
  "
5059
6600
  `;
5060
6601
 
6602
+ exports[`Assets Directory Snapshots > Python framework assets > python/python/http/strands/base/skills/fetcher.py should match snapshot 1`] = `
6603
+ """"Skill fetcher — downloads s3/git skills to local filesystem on first use.
6604
+
6605
+ Resolved paths are passed to AgentSkills(skills=...) in main.py.
6606
+ Cache directory: <tmpdir>/.agents/skills/ — an absolute path under the system temp
6607
+ directory (honors $TMPDIR, defaults to /tmp). The runtime working directory (e.g.
6608
+ /var/task in a CodeZip runtime) is read-only, so the cache must live somewhere
6609
+ guaranteed-writable.
6610
+ """
6611
+
6612
+ import base64
6613
+ import hashlib
6614
+ import json
6615
+ import logging
6616
+ import os
6617
+ import shutil
6618
+ import subprocess
6619
+ import tempfile
6620
+ from pathlib import Path
6621
+ from typing import Optional
6622
+
6623
+ logger = logging.getLogger(__name__)
6624
+
6625
+ _SKILLS_BASE = Path(tempfile.gettempdir()) / ".agents" / "skills"
6626
+ _GIT_TIMEOUT = 60
6627
+ _S3_MAX_SIZE_BYTES = 1 * 1024 * 1024 * 1024 # 1 GB
6628
+
6629
+
6630
+ def _stable_hash(value: str) -> str:
6631
+ return hashlib.sha256(value.encode()).hexdigest()[:12]
6632
+
6633
+
6634
+ def _cleanup(path: Path) -> None:
6635
+ """Remove a partially-created skill directory so retries don't see stale state."""
6636
+ shutil.rmtree(path, ignore_errors=True)
6637
+
6638
+
6639
+ def _read_map(type_dir: Path) -> dict:
6640
+ map_file = type_dir / ".map.json"
6641
+ return json.loads(map_file.read_text()) if map_file.exists() else {}
6642
+
6643
+
6644
+ def _write_map(type_dir: Path, mapping: dict) -> None:
6645
+ type_dir.mkdir(parents=True, exist_ok=True)
6646
+ (type_dir / ".map.json").write_text(json.dumps(mapping))
6647
+
6648
+
6649
+ def _resolve_cached(type_dir: Path, source_hash: str) -> Optional[str]:
6650
+ """Return the cached skill directory for a source hash, or None if not on disk."""
6651
+ mapping = _read_map(type_dir)
6652
+ dir_name = mapping.get(source_hash)
6653
+ if dir_name and (type_dir / dir_name).exists():
6654
+ return str(type_dir / dir_name)
6655
+ return None
6656
+
6657
+
6658
+ def _read_skill_name(skill_dir: Path) -> str:
6659
+ """Extract the skill name from SKILL.md YAML frontmatter."""
6660
+ content = (skill_dir / "SKILL.md").read_text()
6661
+ if not content.startswith("---"):
6662
+ raise ValueError(f"SKILL.md in {skill_dir} has no YAML frontmatter (must start with ---)")
6663
+ parts = content.split("---", 2)
6664
+ if len(parts) < 3:
6665
+ raise ValueError(f"SKILL.md in {skill_dir} has malformed frontmatter (missing closing ---)")
6666
+ for line in parts[1].strip().splitlines():
6667
+ if line.startswith("name:"):
6668
+ name = line[len("name:"):].strip().strip("\\"'")
6669
+ if name:
6670
+ return name
6671
+ raise ValueError(f"SKILL.md in {skill_dir} is missing a 'name' field in frontmatter")
6672
+
6673
+
6674
+ def _pick_dir_name(type_dir: Path, name: str, source_hash: str) -> str:
6675
+ """Pick a unique directory name, appending a hash suffix on collision."""
6676
+ if not (type_dir / name).exists():
6677
+ return name
6678
+ return f"{name}-{source_hash[:8]}"
6679
+
6680
+
6681
+ def _rename_and_cache_skill(type_dir: Path, temp_dir: Path, source_hash: str, skill_root: Path,
6682
+ source_label: str = "") -> Path:
6683
+ """Validate SKILL.md, rename the temp dir to the skill's declared name, and update the map.
6684
+
6685
+ Raises ValueError if SKILL.md is missing or has invalid frontmatter.
6686
+ """
6687
+ if not (skill_root / "SKILL.md").exists():
6688
+ _cleanup(temp_dir)
6689
+ hint = f" (source: {source_label})" if source_label else ""
6690
+ raise ValueError(f"No SKILL.md found in fetched skill{hint}")
6691
+
6692
+ name = _read_skill_name(skill_root)
6693
+ dir_name = _pick_dir_name(type_dir, name, source_hash)
6694
+ final_dir = type_dir / dir_name
6695
+ if final_dir != temp_dir:
6696
+ temp_dir.rename(final_dir)
6697
+
6698
+ mapping = _read_map(type_dir)
6699
+ mapping[source_hash] = dir_name
6700
+ _write_map(type_dir, mapping)
6701
+ return final_dir
6702
+
6703
+
6704
+ def _fetch_s3_skill(source: str, s3_client=None) -> Path:
6705
+ """Download an s3:// skill prefix and return the local directory."""
6706
+ uri = source if source.endswith("/") else source + "/"
6707
+ source_hash = _stable_hash(uri)
6708
+ type_dir = _SKILLS_BASE / "s3"
6709
+
6710
+ cached = _resolve_cached(type_dir, source_hash)
6711
+ if cached:
6712
+ return Path(cached)
6713
+
6714
+ import boto3
6715
+ client = s3_client or boto3.client("s3")
6716
+ bucket, _, prefix = uri[len("s3://"):].partition("/")
6717
+ if not bucket:
6718
+ raise ValueError(f"Invalid S3 URI (no bucket): {uri}")
6719
+
6720
+ temp_dir = type_dir / source_hash
6721
+ _cleanup(temp_dir)
6722
+ temp_dir.mkdir(parents=True, exist_ok=True)
6723
+ temp_root = temp_dir.resolve()
6724
+
6725
+ paginator = client.get_paginator("list_objects_v2")
6726
+ total = 0
6727
+ for page in paginator.paginate(Bucket=bucket, Prefix=prefix):
6728
+ for obj in page.get("Contents", []):
6729
+ total += obj["Size"]
6730
+ if total > _S3_MAX_SIZE_BYTES:
6731
+ _cleanup(temp_dir)
6732
+ raise ValueError(f"S3 skill {uri} exceeds 1 GB size limit")
6733
+ rel = obj["Key"][len(prefix):].lstrip("/")
6734
+ if not rel:
6735
+ continue
6736
+ dest = (temp_dir / rel).resolve()
6737
+ if dest != temp_root and not str(dest).startswith(str(temp_root) + os.sep):
6738
+ _cleanup(temp_dir)
6739
+ raise ValueError(f"Path traversal detected in S3 key: {obj['Key']}")
6740
+ dest.parent.mkdir(parents=True, exist_ok=True)
6741
+ client.download_file(bucket, obj["Key"], str(dest))
6742
+
6743
+ if total == 0:
6744
+ _cleanup(temp_dir)
6745
+ raise ValueError(f"No files found at S3 URI: {uri}")
6746
+
6747
+ return _rename_and_cache_skill(type_dir, temp_dir, source_hash, temp_dir, source_label=uri)
6748
+
6749
+
6750
+ def _resolve_credential_arn(credential_arn: str, identity_client) -> str:
6751
+ """Resolve a Token Vault API-key credential ARN to its secret value via AgentCore Identity.
6752
+
6753
+ ARN format: arn:<p>:bedrock-agentcore:<region>:<account>:token-vault/<vault>/apikeycredentialprovider/<name>
6754
+ """
6755
+ from bedrock_agentcore.runtime.context import BedrockAgentCoreContext # noqa: PLC0415
6756
+
6757
+ provider_name = credential_arn.rsplit("/", 1)[-1]
6758
+ if not provider_name:
6759
+ raise ValueError(f"Invalid credential ARN: {credential_arn}")
6760
+ workload_token = BedrockAgentCoreContext.get_workload_access_token()
6761
+ if not workload_token:
6762
+ raise ValueError("Credential ARN resolution requires a workload access token")
6763
+ api_key = identity_client.dp_client.get_resource_api_key(
6764
+ resourceCredentialProviderName=provider_name,
6765
+ workloadIdentityToken=workload_token,
6766
+ )["apiKey"]
6767
+ if not api_key:
6768
+ raise ValueError(f"Identity returned empty API key for provider: {provider_name}")
6769
+ return api_key
6770
+
6771
+
6772
+ def _build_git_auth_env(credential_arn: Optional[str], username: Optional[str], identity_client=None) -> dict:
6773
+ """Build GIT_CONFIG_* env vars for HTTP Basic auth using a Token Vault credential ARN.
6774
+
6775
+ Uses env vars instead of -c args to avoid leaking credentials in /proc/*/cmdline,
6776
+ and so auth propagates to sub-commands (e.g. sparse-checkout triggering a fetch).
6777
+ """
6778
+ if not credential_arn or not identity_client:
6779
+ return {}
6780
+ password = _resolve_credential_arn(credential_arn, identity_client)
6781
+ user = username or "oauth2"
6782
+ encoded = base64.b64encode(f"{user}:{password}".encode()).decode()
6783
+ return {
6784
+ "GIT_CONFIG_COUNT": "1",
6785
+ "GIT_CONFIG_KEY_0": "http.extraHeader",
6786
+ "GIT_CONFIG_VALUE_0": f"Authorization: Basic {encoded}",
6787
+ }
6788
+
6789
+
6790
+ def _fetch_git_skill(url: str, skill_path: str = "", credential_arn: Optional[str] = None,
6791
+ username: Optional[str] = None, identity_client=None) -> Path:
6792
+ """Shallow-clone a git skill repository and return the local skill directory.
6793
+
6794
+ Returns the directory containing SKILL.md (the subdir itself for sparse checkouts).
6795
+ """
6796
+ if skill_path and (os.path.isabs(skill_path) or ".." in Path(skill_path).parts):
6797
+ raise ValueError(f"Path traversal detected in skill path: {skill_path}")
6798
+
6799
+ source_hash = _stable_hash(f"{url}:{skill_path}")
6800
+ type_dir = _SKILLS_BASE / "git"
6801
+
6802
+ cached = _resolve_cached(type_dir, source_hash)
6803
+ if cached:
6804
+ return Path(cached) / skill_path if skill_path else Path(cached)
6805
+
6806
+ temp_dir = type_dir / source_hash
6807
+ _cleanup(temp_dir)
6808
+ temp_dir.mkdir(parents=True, exist_ok=True)
6809
+
6810
+ extra_env = _build_git_auth_env(credential_arn, username, identity_client)
6811
+ git_env = {**os.environ, **extra_env} if extra_env else None
6812
+
6813
+ try:
6814
+ if skill_path:
6815
+ subprocess.run(
6816
+ ["git", "clone", "--depth", "1", "--filter=blob:none", "--sparse", url, str(temp_dir)],
6817
+ check=True, timeout=_GIT_TIMEOUT, capture_output=True, env=git_env,
6818
+ )
6819
+ subprocess.run(
6820
+ ["git", "sparse-checkout", "set", skill_path],
6821
+ check=True, timeout=_GIT_TIMEOUT, capture_output=True, cwd=str(temp_dir), env=git_env,
6822
+ )
6823
+ else:
6824
+ subprocess.run(
6825
+ ["git", "clone", "--depth", "1", url, str(temp_dir)],
6826
+ check=True, timeout=_GIT_TIMEOUT, capture_output=True, env=git_env,
6827
+ )
6828
+ except Exception:
6829
+ _cleanup(temp_dir)
6830
+ raise
6831
+
6832
+ if skill_path and not (temp_dir / skill_path).exists():
6833
+ _cleanup(temp_dir)
6834
+ raise ValueError(f"Skill path '{skill_path}' not found in repository '{url}'")
6835
+
6836
+ # SKILL.md lives inside the subdir for sparse checkouts.
6837
+ skill_root = temp_dir / skill_path if skill_path else temp_dir
6838
+ label = f"{url}:{skill_path}" if skill_path else url
6839
+ final_dir = _rename_and_cache_skill(type_dir, temp_dir, source_hash, skill_root, source_label=label)
6840
+ return final_dir / skill_path if skill_path else final_dir
6841
+
6842
+
6843
+ def resolve_s3_skills(sources: list, s3_client=None) -> list:
6844
+ """Resolve s3:// skill URIs to local filesystem paths.
6845
+
6846
+ Any fetch failure raises and fails the invocation — a partial skill set
6847
+ would silently run the agent without capabilities the harness declared.
6848
+ """
6849
+ paths = []
6850
+ for uri in sources:
6851
+ try:
6852
+ skill_dir = _fetch_s3_skill(uri, s3_client)
6853
+ except Exception as e:
6854
+ raise ValueError(f"Failed to resolve S3 skill '{uri}': {e}") from e
6855
+ paths.append(str(skill_dir.resolve()))
6856
+ return paths
6857
+
6858
+
6859
+ def resolve_git_skills(sources: list, identity_client=None) -> list:
6860
+ """Resolve git skill dicts to local filesystem paths.
6861
+
6862
+ Each source is a dict with keys: url (required), path (optional),
6863
+ credentialArn (optional), username (optional).
6864
+
6865
+ Any fetch failure raises and fails the invocation — a partial skill set
6866
+ would silently run the agent without capabilities the harness declared.
6867
+ """
6868
+ paths = []
6869
+ for source in sources:
6870
+ try:
6871
+ skill_dir = _fetch_git_skill(
6872
+ url=source["url"],
6873
+ skill_path=source.get("path") or "",
6874
+ credential_arn=source.get("credentialArn"),
6875
+ username=source.get("username"),
6876
+ identity_client=identity_client,
6877
+ )
6878
+ except Exception as e:
6879
+ raise ValueError(f"Failed to resolve git skill '{source.get('url', source)}': {e}") from e
6880
+ paths.append(str(skill_dir.resolve()))
6881
+ return paths
6882
+ "
6883
+ `;
6884
+
6885
+ exports[`Assets Directory Snapshots > Python framework assets > python/python/http/strands/capabilities/execution-limits/hooks/execution_limits.py should match snapshot 1`] = `
6886
+ "import time
6887
+ from typing import Optional
6888
+
6889
+ from strands.hooks import BeforeModelCallEvent
6890
+ from strands.hooks.registry import HookProvider, HookRegistry
6891
+ from strands.types.exceptions import EventLoopException
6892
+
6893
+
6894
+ class ExecutionLimitExceeded(Exception):
6895
+ def __init__(self, message: str) -> None:
6896
+ super().__init__(message)
6897
+
6898
+
6899
+ class ExecutionLimitsHook(HookProvider):
6900
+ def __init__(
6901
+ self,
6902
+ max_iterations: Optional[int] = None,
6903
+ max_tokens: Optional[int] = None,
6904
+ timeout_seconds: Optional[float] = None,
6905
+ ) -> None:
6906
+ self._max_iterations = max_iterations
6907
+ self._max_tokens = max_tokens
6908
+ self._timeout_seconds = timeout_seconds
6909
+ self._iteration_count = 0
6910
+ self._start_time = time.monotonic()
6911
+
6912
+ def register_hooks(self, registry: HookRegistry, **kwargs) -> None:
6913
+ registry.add_callback(BeforeModelCallEvent, self._check_limits)
6914
+
6915
+ def _check_limits(self, event: BeforeModelCallEvent) -> None:
6916
+ self._iteration_count += 1
6917
+
6918
+ if self._max_iterations is not None and self._iteration_count > self._max_iterations:
6919
+ raise EventLoopException(
6920
+ ExecutionLimitExceeded(f"Max iterations exceeded: {self._max_iterations}")
6921
+ )
6922
+
6923
+ if self._timeout_seconds is not None:
6924
+ elapsed = time.monotonic() - self._start_time
6925
+ if elapsed > self._timeout_seconds:
6926
+ raise EventLoopException(
6927
+ ExecutionLimitExceeded(
6928
+ f"Timeout exceeded: {self._timeout_seconds}s (elapsed {elapsed:.1f}s)"
6929
+ )
6930
+ )
6931
+
6932
+ if self._max_tokens is not None:
6933
+ used = event.agent.event_loop_metrics.accumulated_usage.get("outputTokens", 0)
6934
+ if used >= self._max_tokens:
6935
+ raise EventLoopException(
6936
+ ExecutionLimitExceeded(
6937
+ f"Max output tokens exceeded: {used}/{self._max_tokens}"
6938
+ )
6939
+ )
6940
+ "
6941
+ `;
6942
+
5061
6943
  exports[`Assets Directory Snapshots > Python framework assets > python/python/http/strands/capabilities/memory/__init__.py should match snapshot 1`] = `
5062
6944
  "# Package marker
5063
6945
  "
@@ -5090,8 +6972,11 @@ def get_memory_session_manager(session_id: Optional[str], actor_id: str) -> Opti
5090
6972
  {{#if (includes memoryProviders.[0].strategies "USER_PREFERENCE")}}
5091
6973
  f"/users/{actor_id}/preferences": RetrievalConfig(top_k=3, relevance_score=0.5),
5092
6974
  {{/if}}
6975
+ {{#if (includes memoryProviders.[0].strategies "EPISODIC")}}
6976
+ f"/episodes/{actor_id}/{session_id}": RetrievalConfig(top_k=5, relevance_score=0.5),
6977
+ {{/if}}
5093
6978
  {{#if (includes memoryProviders.[0].strategies "SUMMARIZATION")}}
5094
- f"/summaries/{actor_id}/{session_id}": RetrievalConfig(top_k=3, relevance_score=0.5),
6979
+ f"/summaries/{actor_id}": RetrievalConfig(top_k=3, relevance_score=0.5),
5095
6980
  {{/if}}
5096
6981
  }
5097
6982
  {{/if}}
@@ -5111,6 +6996,160 @@ def get_memory_session_manager(session_id: Optional[str], actor_id: str) -> Opti
5111
6996
  "
5112
6997
  `;
5113
6998
 
6999
+ exports[`Assets Directory Snapshots > Python framework assets > python/python/http/strands/capabilities/payments/__init__.py should match snapshot 1`] = `
7000
+ """"Payment capabilities for Strands agents."""
7001
+ from .payments import create_payments_plugin
7002
+
7003
+ __all__ = ["create_payments_plugin"]
7004
+ "
7005
+ `;
7006
+
7007
+ exports[`Assets Directory Snapshots > Python framework assets > python/python/http/strands/capabilities/payments/payments.py should match snapshot 1`] = `
7008
+ """"Payment capability -- auto-generated by agentcore CLI.
7009
+
7010
+ Configures AgentCorePaymentsPlugin for Strands agents.
7011
+ Manager config is auto-discovered from AGENTCORE_PAYMENT_*
7012
+ environment variables set at deploy time.
7013
+
7014
+ Uses a per-invocation factory pattern so each request gets its own
7015
+ plugin instance with the correct user_id, instrument_id, and session_id.
7016
+ This prevents concurrency bugs where one user's payment context
7017
+ could leak to another user's request.
7018
+
7019
+ Uses the official SDK plugin which handles:
7020
+ - x402 v1 (body-based) and v2 (header-based) payment detection
7021
+ - Automatic 402 response interception and payment processing
7022
+ - Retry limiting (max 3 payment retries per tool use)
7023
+ - Error management and logging
7024
+ """
7025
+ import os
7026
+ import logging
7027
+
7028
+ import boto3
7029
+ from bedrock_agentcore.payments.integrations.strands import AgentCorePaymentsPlugin
7030
+ from bedrock_agentcore.payments.integrations.config import AgentCorePaymentsPluginConfig
7031
+
7032
+ logger = logging.getLogger(__name__)
7033
+
7034
+ PAYMENT_SYSTEM_PROMPT = """
7035
+ You have payment capabilities via the x402 protocol:
7036
+ - Use http_request to call HTTP endpoints. 402 Payment Required responses are settled automatically by the plugin and the call is retried.
7037
+ - Use get_payment_session to check your remaining budget before expensive operations
7038
+ - Use get_payment_instrument_balance to check wallet USDC balance
7039
+ - Use list_payment_instruments to see available payment instruments
7040
+ - If budget is low, inform the user before proceeding with paid requests
7041
+ """
7042
+
7043
+ _manager_arn = None
7044
+ _connector_id = None
7045
+ _process_payment_role_arn = None
7046
+ _name_segment = None
7047
+ _region = None
7048
+ _auth_mode = None
7049
+ _manager_count = 0
7050
+ for key, value in os.environ.items():
7051
+ if key.startswith("AGENTCORE_PAYMENT_") and key.endswith("_MANAGER_ARN"):
7052
+ if _manager_arn is None:
7053
+ _manager_arn = value
7054
+ _name_segment = key[len("AGENTCORE_PAYMENT_"):-len("_MANAGER_ARN")]
7055
+ _manager_count += 1
7056
+ if _manager_count > 1:
7057
+ logger.warning(
7058
+ "Multiple payment managers detected in environment. Using the first one found. "
7059
+ "Remove extra AGENTCORE_PAYMENT_*_MANAGER_ARN env vars to eliminate ambiguity."
7060
+ )
7061
+ _region = os.getenv("AWS_REGION")
7062
+
7063
+ _prefix = f"AGENTCORE_PAYMENT_{_name_segment}_" if _name_segment else "AGENTCORE_PAYMENT_"
7064
+ _auth_mode = os.getenv(f"{_prefix}AUTH_MODE", "sigv4")
7065
+ _connector_id = os.getenv(f"{_prefix}CONNECTOR_ID")
7066
+ _process_payment_role_arn = os.getenv(f"{_prefix}PROCESS_PAYMENT_ROLE_ARN")
7067
+ _auto_payment = os.getenv(f"{_prefix}AUTO_PAYMENT", "true").lower() == "true"
7068
+ _allowlist_raw = os.getenv(f"{_prefix}TOOL_ALLOWLIST")
7069
+ _allowlist = _allowlist_raw.split(",") if _allowlist_raw else None
7070
+ _network_prefs_raw = os.getenv(f"{_prefix}NETWORK_PREFERENCES")
7071
+ _network_prefs = _network_prefs_raw.split(",") if _network_prefs_raw else None
7072
+
7073
+ if not _manager_arn:
7074
+ logger.warning("No payment manager config found in environment")
7075
+ if not _connector_id:
7076
+ logger.warning("No payment connector config found in environment")
7077
+
7078
+
7079
+ def _assume_role_session(role_arn):
7080
+ """Assume an IAM role and return a boto3 session with temporary credentials."""
7081
+ sts = boto3.client("sts", region_name=_region)
7082
+ creds = sts.assume_role(
7083
+ RoleArn=role_arn,
7084
+ RoleSessionName="agentcore-payment-plugin",
7085
+ )["Credentials"]
7086
+ return boto3.Session(
7087
+ aws_access_key_id=creds["AccessKeyId"],
7088
+ aws_secret_access_key=creds["SecretAccessKey"],
7089
+ aws_session_token=creds["SessionToken"],
7090
+ region_name=_region,
7091
+ )
7092
+
7093
+
7094
+ def create_payments_plugin(user_id, instrument_id=None, session_id=None):
7095
+ """Create a fresh plugin instance per invocation.
7096
+
7097
+ Args:
7098
+ user_id: From invocation context (required for SigV4, derived from JWT for bearer)
7099
+ instrument_id: From invocation payload (created by app backend per user)
7100
+ session_id: From invocation payload (created by app backend per conversation)
7101
+
7102
+ Returns:
7103
+ AgentCorePaymentsPlugin instance, or None if no manager is configured.
7104
+ """
7105
+ if not _manager_arn:
7106
+ return None
7107
+
7108
+ config_kwargs = {
7109
+ "payment_manager_arn": _manager_arn,
7110
+ "region": _region,
7111
+ "payment_instrument_id": instrument_id,
7112
+ "payment_session_id": session_id,
7113
+ "payment_connector_id": _connector_id,
7114
+ }
7115
+
7116
+ config_kwargs["auto_payment"] = _auto_payment
7117
+ if _allowlist:
7118
+ config_kwargs["payment_tool_allowlist"] = _allowlist
7119
+ if _network_prefs:
7120
+ config_kwargs["network_preferences_config"] = _network_prefs
7121
+
7122
+ if _process_payment_role_arn:
7123
+ # Only pass boto3_session if SDK supports it (added in bedrock-agentcore >= 1.11).
7124
+ # Older SDKs use the runtime role's default credentials and can still call ProcessPayment
7125
+ # if the runtime role has been granted permission directly.
7126
+ import inspect
7127
+ if "boto3_session" in inspect.signature(AgentCorePaymentsPluginConfig).parameters:
7128
+ config_kwargs["boto3_session"] = _assume_role_session(_process_payment_role_arn)
7129
+ else:
7130
+ logger.warning(
7131
+ "PROCESS_PAYMENT_ROLE_ARN set but bedrock-agentcore SDK does not support boto3_session. "
7132
+ "Upgrade to bedrock-agentcore>=1.11 to enable cross-role payment processing."
7133
+ )
7134
+
7135
+ if _auth_mode == "bearer":
7136
+ bearer_token = os.getenv("AGENTCORE_BEARER_TOKEN")
7137
+ if bearer_token:
7138
+ config_kwargs["bearer_token"] = bearer_token
7139
+ else:
7140
+ logger.warning(
7141
+ "Bearer auth mode configured but AGENTCORE_BEARER_TOKEN not set. "
7142
+ "Falling back to SigV4. Set AGENTCORE_BEARER_TOKEN or pass bearer_token in invoke context."
7143
+ )
7144
+ config_kwargs["user_id"] = user_id or "default-user"
7145
+ else:
7146
+ config_kwargs["user_id"] = user_id or "default-user"
7147
+
7148
+ config = AgentCorePaymentsPluginConfig(**config_kwargs)
7149
+ return AgentCorePaymentsPlugin(config=config)
7150
+ "
7151
+ `;
7152
+
5114
7153
  exports[`Assets Directory Snapshots > Python framework assets > python/python/mcp/standalone/base/README.md should match snapshot 1`] = `
5115
7154
  "# {{ name }}
5116
7155
 
@@ -5197,10 +7236,68 @@ Thumbs.db
5197
7236
  `;
5198
7237
 
5199
7238
  exports[`Assets Directory Snapshots > Python framework assets > python/python/mcp/standalone/base/main.py should match snapshot 1`] = `
5200
- "from mcp.server.fastmcp import FastMCP
7239
+ "{{#if needsOs}}
7240
+ import os
7241
+ {{/if}}
7242
+ from mcp.server.fastmcp import FastMCP
5201
7243
 
5202
7244
  mcp = FastMCP("{{ name }}", host="0.0.0.0", stateless_http=True)
5203
7245
 
7246
+ {{#if needsOs}}
7247
+ _MOUNT_PATHS = [
7248
+ {{#if sessionStorageMountPath}}"{{sessionStorageMountPath}}",{{/if}}
7249
+ {{#each efsMounts}}"{{mountPath}}",{{/each}}
7250
+ {{#each s3Mounts}}"{{mountPath}}",{{/each}}
7251
+ ]
7252
+
7253
+ def _safe_resolve(path: str) -> str:
7254
+ resolved = os.path.realpath(path)
7255
+ if not any(resolved == os.path.realpath(m) or resolved.startswith(os.path.realpath(m) + os.sep) for m in _MOUNT_PATHS):
7256
+ raise ValueError(f"Path '{path}' is not within any configured mount ({', '.join(_MOUNT_PATHS)})")
7257
+ return resolved
7258
+
7259
+ @mcp.tool()
7260
+ def file_read(path: str) -> str:
7261
+ """Read a file from a mounted filesystem. Use the absolute path (e.g. /mnt/tools/data.txt)."""
7262
+ try:
7263
+ full_path = _safe_resolve(path)
7264
+ with open(full_path) as f:
7265
+ return f.read()
7266
+ except ValueError as e:
7267
+ return str(e)
7268
+ except OSError as e:
7269
+ return f"Error reading '{path}': {e.strerror}"
7270
+
7271
+ @mcp.tool()
7272
+ def file_write(path: str, content: str) -> str:
7273
+ """Write a file to a mounted filesystem. Use the absolute path (e.g. /mnt/tools/data.txt)."""
7274
+ try:
7275
+ full_path = _safe_resolve(path)
7276
+ parent = os.path.dirname(full_path)
7277
+ if parent:
7278
+ os.makedirs(parent, exist_ok=True)
7279
+ with open(full_path, "w") as f:
7280
+ f.write(content)
7281
+ return f"Written to {path}"
7282
+ except ValueError as e:
7283
+ return str(e)
7284
+ except OSError as e:
7285
+ return f"Error writing '{path}': {e.strerror}"
7286
+
7287
+ @mcp.tool()
7288
+ def list_files(path: str) -> str:
7289
+ """List files in a mounted filesystem directory. Use the absolute path (e.g. /mnt/tools)."""
7290
+ try:
7291
+ full_path = _safe_resolve(path)
7292
+ entries = os.listdir(full_path)
7293
+ return "\\n".join(entries) if entries else "(empty directory)"
7294
+ except ValueError as e:
7295
+ return str(e)
7296
+ except OSError as e:
7297
+ return f"Error listing '{path}': {e.strerror}"
7298
+
7299
+ {{/if}}
7300
+
5204
7301
 
5205
7302
  @mcp.tool()
5206
7303
  def add_numbers(a: int, b: int) -> int:
@@ -5237,7 +7334,7 @@ description = "AgentCore MCP Server"
5237
7334
  readme = "README.md"
5238
7335
  requires-python = ">=3.10"
5239
7336
  dependencies = [
5240
- "mcp >= 1.19.0",
7337
+ "mcp ~= 1.24.0",
5241
7338
  ]
5242
7339
 
5243
7340
  [tool.hatch.build.targets.wheel]
@@ -5305,7 +7402,7 @@ For Container builds, \`BaseRenderer.render()\` automatically copies the \`conta
5305
7402
 
5306
7403
  - These assets are rendered by the CLI's template renderer in \`src/cli/templates/\`
5307
7404
  - The \`README.md\` and \`agents/AGENTS.md\` are copied verbatim (no template rendering) during project creation
5308
- - The \`.llm-context/\` files are sourced from \`src/schema/llm-compacted/\` and written during init
7405
+ - The \`.llm-context/\` files are sourced from \`src/schema/llm-compacted/\` and written during project creation
5309
7406
  "
5310
7407
  `;
5311
7408
 
@@ -5325,8 +7422,7 @@ my-project/
5325
7422
  │ ├── .env.local # Secrets — API keys (gitignored)
5326
7423
  │ ├── .llm-context/ # TypeScript type definitions for AI assistants
5327
7424
  │ │ ├── agentcore.ts # AgentCoreProjectSpec types
5328
- │ │ ├── aws-targets.ts # Deployment target types
5329
- │ │ └── mcp.ts # Gateway and MCP tool types
7425
+ │ │ └── aws-targets.ts # Deployment target types
5330
7426
  │ └── cdk/ # CDK infrastructure (@aws/agentcore-cdk)
5331
7427
  ├── app/ # Agent application code
5332
7428
  └── evaluators/ # Custom evaluator code (if any)
@@ -5349,6 +7445,11 @@ Run your agent locally:
5349
7445
  agentcore dev
5350
7446
  \`\`\`
5351
7447
 
7448
+ ### Validate Invocation Input
7449
+
7450
+ Validate runtime invocation payloads before forwarding them to an agent framework. Keep user prompts typed as strings
7451
+ and pass only prompt text to the agent.
7452
+
5352
7453
  ### Deployment
5353
7454
 
5354
7455
  Deploy to AWS:
@@ -5443,6 +7544,8 @@ Tags defined in \`agentcore.json\` flow through to deployed CloudFormation resou
5443
7544
  \`agentcore validate\` to check.
5444
7545
  4. **Resource Removal:** Use \`agentcore remove\` to remove resources. Run \`agentcore deploy\` after removal to tear down
5445
7546
  deployed infrastructure.
7547
+ 5. **Invocation Input:** Validate runtime payloads and require text prompts to be strings. If a Strands app accepts a
7548
+ caller-supplied message history, normalize the history tail with \`strip_trailing_tool_use()\` before invocation.
5446
7549
 
5447
7550
  ## Directory Structure
5448
7551
 
@@ -5456,8 +7559,7 @@ myProject/
5456
7559
  │ ├── .llm-context/ # TypeScript type definitions for AI assistants
5457
7560
  │ │ ├── README.md # Guide to using schema files
5458
7561
  │ │ ├── agentcore.ts # AgentCoreProjectSpec types
5459
- │ │ ├── aws-targets.ts # AWS deployment target types
5460
- │ │ └── mcp.ts # Gateway and MCP tool types
7562
+ │ │ └── aws-targets.ts # AWS deployment target types
5461
7563
  │ └── cdk/ # AWS CDK project (@aws/agentcore-cdk L3 constructs)
5462
7564
  ├── app/ # Agent application code
5463
7565
  └── evaluators/ # Custom evaluator code (if any)
@@ -5471,18 +7573,25 @@ file maps to a JSON config file and includes validation constraints as comments
5471
7573
  | JSON Config | Schema File | Root Type |
5472
7574
  | --- | --- | --- |
5473
7575
  | \`agentcore/agentcore.json\` | \`agentcore/.llm-context/agentcore.ts\` | \`AgentCoreProjectSpec\` |
5474
- | \`agentcore/agentcore.json\` (gateways) | \`agentcore/.llm-context/mcp.ts\` | \`AgentCoreMcpSpec\` |
5475
7576
  | \`agentcore/aws-targets.json\` | \`agentcore/.llm-context/aws-targets.ts\` | \`AwsDeploymentTarget[]\` |
5476
7577
 
5477
7578
  ### Key Types
5478
7579
 
5479
- - **AgentCoreProjectSpec**: Root config with \`runtimes\`, \`memories\`, \`credentials\`, \`agentCoreGateways\`, \`evaluators\`, \`onlineEvalConfigs\`, \`policyEngines\` arrays
7580
+ - **AgentCoreProjectSpec**: Root config with runtimes, memories, knowledge bases, credentials, evaluators, online evals and insights, gateways, policy engines, config bundles, A/B tests, harness registrations, datasets, payment managers, and capacity providers
5480
7581
  - **AgentEnvSpec**: Agent configuration (build type, entrypoint, code location, runtime version, network mode)
5481
7582
  - **Memory**: Memory resource with strategies (SEMANTIC, SUMMARIZATION, USER_PREFERENCE, EPISODIC) and expiry
5482
7583
  - **Credential**: API key or OAuth credential provider
5483
- - **AgentCoreGateway**: MCP gateway with targets (Lambda, MCP server, OpenAPI, Smithy, API Gateway)
7584
+ - **AgentCoreGateway**: MCP gateway with targets (Lambda, MCP server, OpenAPI, Smithy, API Gateway, web-search, knowledge-base)
5484
7585
  - **Evaluator**: LLM-as-a-Judge or code-based evaluator
5485
7586
  - **OnlineEvalConfig**: Continuous evaluation pipeline bound to an agent
7587
+ - **OnlineInsightsConfig** _[preview]_: Continuous failure-pattern analysis bound to an agent
7588
+ - **KnowledgeBase**: Managed Bedrock Knowledge Base auto-wired to a gateway
7589
+ - **Harness**: Declarative agent — runtime + tools + skills + memory + observability without writing agent code
7590
+ - **PolicyEngine** + **Policy**: Cedar policy engine with form-based guardrails (Bedrock content filters, prompt-attack, sensitive-info) or raw Cedar policies
7591
+ - **PaymentManager** + **PaymentConnector**: x402-protocol payment orchestration with provider credentials (CoinbaseCDP, StripePrivy)
7592
+ - **ConfigBundle**: Versioned runtime configuration as a separately-deployable resource
7593
+ - **Dataset**: Curated session dataset for batch evaluation and recommendation runs
7594
+ - **RuntimeEndpoint**: Named endpoint (e.g. \`PROMPT_V1\`) targeting a specific runtime version
5486
7595
 
5487
7596
  ### Common Enum Values
5488
7597
 
@@ -5490,8 +7599,11 @@ file maps to a JSON config file and includes validation constraints as comments
5490
7599
  - **NetworkMode**: \`'PUBLIC'\` | \`'VPC'\`
5491
7600
  - **RuntimeVersion**: \`'PYTHON_3_10'\` | \`'PYTHON_3_11'\` | \`'PYTHON_3_12'\` | \`'PYTHON_3_13'\` | \`'PYTHON_3_14'\` | \`'NODE_18'\` | \`'NODE_20'\` | \`'NODE_22'\`
5492
7601
  - **MemoryStrategyType**: \`'SEMANTIC'\` | \`'SUMMARIZATION'\` | \`'USER_PREFERENCE'\` | \`'EPISODIC'\`
5493
- - **GatewayTargetType**: \`'lambda'\` | \`'mcpServer'\` | \`'openApiSchema'\` | \`'smithyModel'\` | \`'apiGateway'\` | \`'lambdaFunctionArn'\`
7602
+ - **GatewayTargetType**: \`'lambda'\` | \`'mcpServer'\` | \`'openApiSchema'\` | \`'smithyModel'\` | \`'apiGateway'\` | \`'lambdaFunctionArn'\` | \`'connector'\` (web-search, bedrock-knowledge-bases)
5494
7603
  - **ModelProvider**: \`'Bedrock'\` | \`'Gemini'\` | \`'OpenAI'\` | \`'Anthropic'\`
7604
+ - **PaymentProvider**: \`'CoinbaseCDP'\` | \`'StripePrivy'\`
7605
+ - **PolicyEnforcementMode**: \`'ACTIVE'\` | \`'PASSIVE'\`
7606
+ - **GuardrailContentFilter**: \`'VIOLENCE'\` | \`'HATE'\` | \`'SEXUAL'\` | \`'MISCONDUCT'\` | \`'INSULTS'\`
5495
7607
 
5496
7608
  ### Build Types
5497
7609
 
@@ -5542,24 +7654,890 @@ When modifying JSON config files:
5542
7654
  4. Use CloudFormation-safe names (alphanumeric, start with letter)
5543
7655
  5. Run \`agentcore validate\` to verify changes
5544
7656
 
7657
+ ## Harness Export
7658
+
7659
+ \`agentcore export harness\` converts a harness configuration into a deployable Strands Python agent under \`app/<agentName>/\`.
7660
+
7661
+ **After every export, you MUST read \`app/<agentName>/EXPORT_NOTES.md\` before proceeding.**
7662
+
7663
+ This file lists any manual follow-up items required before the agent will deploy or run correctly — missing files to create, IAM policies to add, or configuration steps the exporter could not automate. A clean export produces "No manual steps required." Complete every item in the file before running \`agentcore deploy\`.
7664
+
7665
+ \`\`\`bash
7666
+ agentcore export harness --name <harnessName> # generates app/<agentName>/EXPORT_NOTES.md
7667
+ cat app/<agentName>/EXPORT_NOTES.md # read this before touching anything else
7668
+ \`\`\`
7669
+
5545
7670
  ## CLI Commands
5546
7671
 
7672
+ Run \`agentcore --help\` or \`agentcore <command> --help\` for full flags. Commonly used:
7673
+
7674
+ **Project lifecycle**
7675
+
5547
7676
  | Command | Description |
5548
7677
  | --- | --- |
5549
7678
  | \`agentcore create\` | Create a new project |
5550
- | \`agentcore add <resource>\` | Add agent, memory, credential, gateway, evaluator, policy |
5551
- | \`agentcore remove <resource>\` | Remove a resource |
5552
7679
  | \`agentcore dev\` | Run agent locally with hot-reload |
5553
7680
  | \`agentcore deploy\` | Deploy to AWS |
5554
- | \`agentcore status\` | Show deployment status |
5555
7681
  | \`agentcore invoke\` | Invoke agent (local or deployed) |
5556
- | \`agentcore logs\` | View agent logs |
5557
- | \`agentcore traces\` | View agent traces |
5558
- | \`agentcore eval\` | Run evaluations against an agent |
5559
- | \`agentcore package\` | Package agent artifacts |
7682
+ | \`agentcore capacity-provider delete-session\` | Delete (deprovision) a live capacity provider session |
7683
+ | \`agentcore status\` | Show deployment status |
5560
7684
  | \`agentcore validate\` | Validate configuration |
5561
- | \`agentcore pause\` / \`resume\` | Pause or resume a deployed agent |
7685
+ | \`agentcore package\` | Package agent artifacts |
7686
+ | \`agentcore import\` | Import resources from a Bedrock AgentCore Starter Toolkit project |
7687
+
7688
+ **Resources**
7689
+
7690
+ | Command | Description |
7691
+ | --- | --- |
7692
+ | \`agentcore add <resource>\` | Add agent, memory, credential, gateway, gateway-target, evaluator, online-eval, online-insights, knowledge-base, harness, policy-engine, policy, payment-manager, payment-connector, capacity-provider, config-bundle, dataset, runtime-endpoint |
7693
+ | \`agentcore remove <resource>\` | Remove any resource |
7694
+ | \`agentcore export harness\` | Export a harness to a Strands runtime agent under \`app/<agentName>/\` |
7695
+
7696
+ **Jobs (run, view, archive, lifecycle)**
7697
+
7698
+ | Command | Description |
7699
+ | --- | --- |
7700
+ | \`agentcore run eval\` | Run on-demand evaluation against agent traces |
7701
+ | \`agentcore run batch-evaluation\` | Run evaluators across all sessions at scale |
7702
+ | \`agentcore run recommendation\` | Optimize prompts or tool descriptions from real traces |
7703
+ | \`agentcore run insights\` _[preview]_ | Run failure-pattern analysis across sessions |
7704
+ | \`agentcore run ab-test\` | Start an A/B test (config-bundle or target-based) |
7705
+ | \`agentcore run ingest\` | Start a fresh ingestion job for every data source on a deployed knowledge base |
7706
+ | \`agentcore view <type>\` | List or view jobs (recommendation, batch-evaluation, ab-test, insights) |
7707
+ | \`agentcore archive <type>\` | Delete a job on the service + clear local history |
7708
+ | \`agentcore stop <type>\` | Stop a running batch-evaluation or ab-test |
7709
+ | \`agentcore promote ab-test\` | Apply the winning variant to \`agentcore.json\` |
7710
+ | \`agentcore pause <type>\` / \`agentcore resume <type>\` | Pause/resume a deployed online-eval, online-insights, or ab-test |
7711
+
7712
+ **Config bundles & datasets**
7713
+
7714
+ | Command | Description |
7715
+ | --- | --- |
7716
+ | \`agentcore config-bundle versions\` (alias \`cb versions\`) | List version history for a bundle |
7717
+ | \`agentcore config-bundle diff\` | Diff two versions of a bundle |
7718
+ | \`agentcore config-bundle create-branch\` | Create a new branch on an existing bundle |
7719
+ | \`agentcore dataset download\` | Download a dataset version locally |
7720
+ | \`agentcore dataset publish-version\` | Publish a new dataset version |
7721
+ | \`agentcore dataset remove-version\` | Remove a dataset version |
7722
+
7723
+ **Observability & history**
7724
+
7725
+ | Command | Description |
7726
+ | --- | --- |
7727
+ | \`agentcore logs\` | Stream/search agent runtime logs |
7728
+ | \`agentcore logs evals\` | Stream/search online-eval logs |
7729
+ | \`agentcore traces list\` / \`agentcore traces get\` | List recent traces or download one to JSON |
7730
+ | \`agentcore evals history\` | View past on-demand eval results |
7731
+
7732
+ **Utilities**
7733
+
7734
+ | Command | Description |
7735
+ | --- | --- |
7736
+ | \`agentcore fetch access\` | Fetch access info for deployed gateway or agent |
7737
+ | \`agentcore feedback\` | Send feedback (with optional screenshot) to the AgentCore team |
7738
+ | \`agentcore update\` | Check for and install CLI updates |
7739
+ | \`agentcore telemetry\` | View or change telemetry preferences |
5562
7740
  "
5563
7741
  `;
5564
7742
 
5565
7743
  exports[`Assets Directory Snapshots > TypeScript assets > typescript/typescript/.gitkeep should match snapshot 1`] = `""`;
7744
+
7745
+ exports[`Assets Directory Snapshots > TypeScript assets > typescript/typescript/http/strands/base/README.md should match snapshot 1`] = `
7746
+ "This is a project generated by the AgentCore CLI!
7747
+
7748
+ # Layout
7749
+
7750
+ The generated application code lives at the agent root directory. At the root, there is a \`.gitignore\` file, an
7751
+ \`agentcore/\` folder which represents the configurations and state associated with this project. Other \`agentcore\`
7752
+ commands like \`deploy\`, \`dev\`, and \`invoke\` rely on the configuration stored here.
7753
+
7754
+ ## Agent Root
7755
+
7756
+ The main entrypoint to your app is defined in \`main.ts\`. Using the AgentCore SDK \`BedrockAgentCoreApp\`, this file
7757
+ defines an HTTP server that streams tokens from your chosen Agent framework SDK.
7758
+
7759
+ \`model/load.ts\` instantiates your chosen model provider.
7760
+
7761
+ ## Input Validation
7762
+
7763
+ The generated Zod request schema keeps plain prompts typed as strings before forwarding them to Strands. Retain this
7764
+ validation when extending the request shape, and pass only prompt text to the agent.
7765
+
7766
+ ## Environment Variables
7767
+
7768
+ | Variable | Required | Description |
7769
+ | --- | --- | --- |
7770
+ {{#if hasIdentity}}| \`{{identityProviders.[0].envVarName}}\` | Yes | {{modelProvider}} API key (local) or Identity provider name (deployed) |
7771
+ {{/if}}| \`LOCAL_DEV\` | No | Set to \`1\` to use \`.env.local\` instead of AgentCore Identity |
7772
+
7773
+ # Developing locally
7774
+
7775
+ If installation was successful, \`node_modules/\` is already populated with dependencies.
7776
+
7777
+ \`agentcore dev\` will start a local server using \`npx tsx watch main.ts\` for hot reload. The port is logged to the terminal (default \`8080\`).
7778
+
7779
+ In a new terminal, you can invoke that server with:
7780
+
7781
+ \`agentcore invoke --dev "What can you do"\`
7782
+
7783
+ # Deployment
7784
+
7785
+ After providing credentials, \`agentcore deploy\` will deploy your project into Amazon Bedrock AgentCore.
7786
+
7787
+ Use \`agentcore invoke\` to invoke your deployed agent.
7788
+ "
7789
+ `;
7790
+
7791
+ exports[`Assets Directory Snapshots > TypeScript assets > typescript/typescript/http/strands/base/gitignore.template should match snapshot 1`] = `
7792
+ "# Environment variables
7793
+ .env
7794
+ .env.*
7795
+
7796
+ # Node
7797
+ node_modules/
7798
+ dist/
7799
+ *.log
7800
+ npm-debug.log*
7801
+ yarn-debug.log*
7802
+ yarn-error.log*
7803
+
7804
+ # IDE
7805
+ .vscode/
7806
+ .idea/
7807
+ *.swp
7808
+ *.swo
7809
+ *~
7810
+
7811
+ # OS
7812
+ .DS_Store
7813
+ Thumbs.db
7814
+ "
7815
+ `;
7816
+
7817
+ exports[`Assets Directory Snapshots > TypeScript assets > typescript/typescript/http/strands/base/main.ts should match snapshot 1`] = `
7818
+ "import { BedrockAgentCoreApp } from 'bedrock-agentcore/runtime';
7819
+ import { Agent, McpClient, tool, type ToolList } from '@strands-agents/sdk';
7820
+ import { z } from 'zod';
7821
+ import { loadModel } from './model/load.js';
7822
+ import { getStreamableHttpMcpClient } from './mcp_client/client.js';
7823
+ {{#if hasMemory}}
7824
+ import { getActorId, getOrCreateMemoryManager } from './memory/memory.js';
7825
+ {{/if}}
7826
+
7827
+ // Define a collection of MCP clients (filter out anything that failed to initialize)
7828
+ const mcpClients: McpClient[] = [getStreamableHttpMcpClient()].filter(
7829
+ (client): client is McpClient => Boolean(client)
7830
+ );
7831
+
7832
+ // Define a collection of tools used by the model
7833
+ const tools: ToolList = [];
7834
+
7835
+ // Define a simple function tool — the Zod schema gives us type inference and runtime validation for free
7836
+ const addNumbers = tool({
7837
+ name: 'add_numbers',
7838
+ description: 'Return the sum of two numbers',
7839
+ inputSchema: z.object({
7840
+ a: z.number(),
7841
+ b: z.number(),
7842
+ }),
7843
+ callback: async ({ a, b }) => a + b,
7844
+ });
7845
+ tools.push(addNumbers);
7846
+
7847
+ // Add MCP clients to tools
7848
+ tools.push(...mcpClients);
7849
+
7850
+ const SYSTEM_PROMPT = \`
7851
+ You are a helpful assistant. Use tools when appropriate.
7852
+ \`;
7853
+
7854
+ const requestSchema = z.object({
7855
+ prompt: z.string().default(''),
7856
+ });
7857
+
7858
+ {{#if hasMemory}}
7859
+ const agentCache = new Map<string, Agent>();
7860
+
7861
+ async function getOrCreateAgent(sessionId: string, actorId: string): Promise<Agent> {
7862
+ const key = \`\${actorId}:\${sessionId}\`;
7863
+ let agent = agentCache.get(key);
7864
+ if (agent) return agent;
7865
+
7866
+ const model = await loadModel();
7867
+ agent = new Agent({
7868
+ model,
7869
+ systemPrompt: SYSTEM_PROMPT,
7870
+ tools,
7871
+ memoryManager: getOrCreateMemoryManager(sessionId, actorId) ?? undefined,
7872
+ });
7873
+ agentCache.set(key, agent);
7874
+ return agent;
7875
+ }
7876
+ {{else}}
7877
+ const AGENT_CACHE_LIMIT = 128;
7878
+
7879
+ // Reuses one Agent per sessionId so each session keeps its own in-process
7880
+ // conversation history (best-effort; resets on cold start). A Map preserves
7881
+ // insertion order, so it doubles as an LRU bounded to 128 sessions — a local
7882
+ // dev process serving many sessions cannot leak history between them or grow
7883
+ // without bound. On AgentCore Runtime each microVM serves a single session, so
7884
+ // this holds one entry. For durable history, attach memory.
7885
+ const agentCache = new Map<string, Agent>();
7886
+
7887
+ async function getOrCreateAgent(sessionId: string): Promise<Agent> {
7888
+ const existing = agentCache.get(sessionId);
7889
+ if (existing) {
7890
+ agentCache.delete(sessionId);
7891
+ agentCache.set(sessionId, existing);
7892
+ return existing;
7893
+ }
7894
+ if (agentCache.size >= AGENT_CACHE_LIMIT) {
7895
+ const oldest = agentCache.keys().next().value;
7896
+ if (oldest !== undefined) agentCache.delete(oldest);
7897
+ }
7898
+ const model = await loadModel();
7899
+ const agent = new Agent({
7900
+ model,
7901
+ systemPrompt: SYSTEM_PROMPT,
7902
+ tools,
7903
+ });
7904
+ agentCache.set(sessionId, agent);
7905
+ return agent;
7906
+ }
7907
+ {{/if}}
7908
+
7909
+ const app = new BedrockAgentCoreApp({
7910
+ invocationHandler: {
7911
+ requestSchema,
7912
+ async *process(payload, context) {
7913
+ {{#if hasMemory}}
7914
+ const sessionId = context?.sessionId ?? 'default-session';
7915
+ const actorId = getActorId(payload, context);
7916
+ const agent = await getOrCreateAgent(sessionId, actorId);
7917
+ {{else}}
7918
+ const sessionId = context?.sessionId ?? 'default-session';
7919
+ const agent = await getOrCreateAgent(sessionId);
7920
+ {{/if}}
7921
+
7922
+ {{#if hasMemory}}
7923
+ try {
7924
+ for await (const event of agent.stream(payload.prompt)) {
7925
+ if (
7926
+ event.type === 'modelStreamUpdateEvent' &&
7927
+ event.event?.type === 'modelContentBlockDeltaEvent' &&
7928
+ event.event.delta?.type === 'textDelta'
7929
+ ) {
7930
+ yield { data: event.event.delta.text };
7931
+ }
7932
+ }
7933
+ } finally {
7934
+ // Drain in-flight createEvent calls before the runtime can reclaim
7935
+ // the session microVM. flush() is the durability mechanism — without
7936
+ // it, an idle reclamation can lose the tail of the conversation.
7937
+ await agent.memoryManager?.flush();
7938
+ }
7939
+ {{else}}
7940
+ // Snapshot history before streaming so a failed turn can be rolled back.
7941
+ // Agent.stream() appends the user message before invoking the model; on a
7942
+ // mid-stream error that user turn would otherwise linger in the cached
7943
+ // agent, and the next turn for this session would send consecutive user
7944
+ // messages (rejected by providers that require strict role alternation,
7945
+ // e.g. Anthropic). Restoring on error keeps the session reusable.
7946
+ const snapshot = agent.takeSnapshot({ include: ['messages'] });
7947
+ try {
7948
+ for await (const event of agent.stream(payload.prompt)) {
7949
+ if (
7950
+ event.type === 'modelStreamUpdateEvent' &&
7951
+ event.event?.type === 'modelContentBlockDeltaEvent' &&
7952
+ event.event.delta?.type === 'textDelta'
7953
+ ) {
7954
+ yield { data: event.event.delta.text };
7955
+ }
7956
+ }
7957
+ } catch (error) {
7958
+ agent.loadSnapshot(snapshot);
7959
+ throw error;
7960
+ }
7961
+ {{/if}}
7962
+ },
7963
+ },
7964
+ });
7965
+
7966
+ app.run({ port: parseInt(process.env.PORT ?? '8080') });
7967
+ "
7968
+ `;
7969
+
7970
+ exports[`Assets Directory Snapshots > TypeScript assets > typescript/typescript/http/strands/base/mcp_client/client.ts should match snapshot 1`] = `
7971
+ "import { McpClient } from '@strands-agents/sdk';
7972
+ import { StreamableHTTPClientTransport } from '@modelcontextprotocol/sdk/client/streamableHttp.js';
7973
+
7974
+ // ExaAI provides information about code through web searches, crawling and code context searches through their platform. Requires no authentication
7975
+ const EXAMPLE_MCP_ENDPOINT = 'https://mcp.exa.ai/mcp';
7976
+
7977
+ export function getStreamableHttpMcpClient(): McpClient {
7978
+ // to use an MCP server that supports bearer authentication, add a headers() callback to requestInit
7979
+ const transport = new StreamableHTTPClientTransport(new URL(EXAMPLE_MCP_ENDPOINT));
7980
+ return new McpClient({ transport });
7981
+ }
7982
+ "
7983
+ `;
7984
+
7985
+ exports[`Assets Directory Snapshots > TypeScript assets > typescript/typescript/http/strands/base/model/load.ts should match snapshot 1`] = `
7986
+ "{{#if (eq modelProvider "Bedrock")}}
7987
+ import { BedrockModel } from '@strands-agents/sdk/models/bedrock';
7988
+
7989
+ export function loadModel(): BedrockModel {
7990
+ return new BedrockModel({ modelId: 'global.anthropic.claude-sonnet-4-5-20250929-v1:0' });
7991
+ }
7992
+ {{/if}}
7993
+ {{#if (eq modelProvider "Anthropic")}}
7994
+ import { AnthropicModel } from '@strands-agents/sdk/models/anthropic';
7995
+ import { withApiKey } from 'bedrock-agentcore/identity';
7996
+
7997
+ const IDENTITY_PROVIDER_NAME = '{{identityProviders.[0].name}}';
7998
+ const IDENTITY_ENV_VAR = '{{identityProviders.[0].envVarName}}';
7999
+
8000
+ async function getApiKey(): Promise<string> {
8001
+ if (process.env.LOCAL_DEV === '1') {
8002
+ const apiKey = process.env[IDENTITY_ENV_VAR] ?? process.env.ANTHROPIC_API_KEY;
8003
+ if (!apiKey) {
8004
+ throw new Error(\`\${IDENTITY_ENV_VAR} or ANTHROPIC_API_KEY not found. Add your key to agentcore/.env.local\`);
8005
+ }
8006
+ return apiKey;
8007
+ }
8008
+ return withApiKey({ providerName: IDENTITY_PROVIDER_NAME })(async (apiKey: string) => apiKey)();
8009
+ }
8010
+
8011
+ let _model: AnthropicModel | undefined;
8012
+
8013
+ export async function loadModel(): Promise<AnthropicModel> {
8014
+ if (!_model) {
8015
+ const apiKey = await getApiKey();
8016
+ _model = new AnthropicModel({
8017
+ apiKey,
8018
+ modelId: 'claude-sonnet-4-5-20250929',
8019
+ maxTokens: 5000,
8020
+ });
8021
+ }
8022
+ return _model;
8023
+ }
8024
+ {{/if}}
8025
+ {{#if (eq modelProvider "OpenAI")}}
8026
+ import { OpenAIModel } from '@strands-agents/sdk/models/openai';
8027
+ import { withApiKey } from 'bedrock-agentcore/identity';
8028
+
8029
+ const IDENTITY_PROVIDER_NAME = '{{identityProviders.[0].name}}';
8030
+ const IDENTITY_ENV_VAR = '{{identityProviders.[0].envVarName}}';
8031
+
8032
+ async function getApiKey(): Promise<string> {
8033
+ if (process.env.LOCAL_DEV === '1') {
8034
+ const apiKey = process.env[IDENTITY_ENV_VAR] ?? process.env.OPENAI_API_KEY;
8035
+ if (!apiKey) {
8036
+ throw new Error(\`\${IDENTITY_ENV_VAR} or OPENAI_API_KEY not found. Add your key to agentcore/.env.local\`);
8037
+ }
8038
+ return apiKey;
8039
+ }
8040
+ return withApiKey({ providerName: IDENTITY_PROVIDER_NAME })(async (apiKey: string) => apiKey)();
8041
+ }
8042
+
8043
+ let _model: OpenAIModel | undefined;
8044
+
8045
+ export async function loadModel(): Promise<OpenAIModel> {
8046
+ if (!_model) {
8047
+ const apiKey = await getApiKey();
8048
+ _model = new OpenAIModel({
8049
+ api: 'chat',
8050
+ apiKey,
8051
+ modelId: 'gpt-4.1',
8052
+ });
8053
+ }
8054
+ return _model;
8055
+ }
8056
+ {{/if}}
8057
+ {{#if (eq modelProvider "Gemini")}}
8058
+ import { GoogleModel } from '@strands-agents/sdk/models/google';
8059
+ import { withApiKey } from 'bedrock-agentcore/identity';
8060
+
8061
+ const IDENTITY_PROVIDER_NAME = '{{identityProviders.[0].name}}';
8062
+ const IDENTITY_ENV_VAR = '{{identityProviders.[0].envVarName}}';
8063
+
8064
+ async function getApiKey(): Promise<string> {
8065
+ if (process.env.LOCAL_DEV === '1') {
8066
+ const apiKey = process.env[IDENTITY_ENV_VAR] ?? process.env.GEMINI_API_KEY;
8067
+ if (!apiKey) {
8068
+ throw new Error(\`\${IDENTITY_ENV_VAR} or GEMINI_API_KEY not found. Add your key to agentcore/.env.local\`);
8069
+ }
8070
+ return apiKey;
8071
+ }
8072
+ return withApiKey({ providerName: IDENTITY_PROVIDER_NAME })(async (apiKey: string) => apiKey)();
8073
+ }
8074
+
8075
+ let _model: GoogleModel | undefined;
8076
+
8077
+ export async function loadModel(): Promise<GoogleModel> {
8078
+ if (!_model) {
8079
+ const apiKey = await getApiKey();
8080
+ _model = new GoogleModel({
8081
+ apiKey,
8082
+ modelId: 'gemini-2.5-flash',
8083
+ });
8084
+ }
8085
+ return _model;
8086
+ }
8087
+ {{/if}}
8088
+ "
8089
+ `;
8090
+
8091
+ exports[`Assets Directory Snapshots > TypeScript assets > typescript/typescript/http/strands/base/package.json should match snapshot 1`] = `
8092
+ "{
8093
+ "name": "{{name}}",
8094
+ "version": "0.1.0",
8095
+ "description": "AgentCore Runtime Application using Strands TypeScript SDK",
8096
+ "private": true,
8097
+ "type": "module",
8098
+ "scripts": {
8099
+ "build": "tsc",
8100
+ "start": "node dist/main.js",
8101
+ "dev": "tsx watch main.ts"
8102
+ },
8103
+ "dependencies": {
8104
+ {{#if (eq modelProvider "Anthropic")}}
8105
+ "@anthropic-ai/sdk": "^0.71.2",
8106
+ {{/if}}
8107
+ {{#if (eq modelProvider "OpenAI")}}
8108
+ "openai": "^6.7.0",
8109
+ {{/if}}
8110
+ {{#if (eq modelProvider "Gemini")}}
8111
+ "@google/genai": "^1.40.0",
8112
+ {{/if}}
8113
+ "@modelcontextprotocol/sdk": "^1.25.2",
8114
+ "@opentelemetry/api": "^1.9.0",
8115
+ "@strands-agents/sdk": "^1.5.0",
8116
+ "bedrock-agentcore": "^0.3.0",
8117
+ "tsx": "^4.19.0",
8118
+ "zod": "^4.4.3"
8119
+ },
8120
+ "devDependencies": {
8121
+ "@types/node": "^22.0.0",
8122
+ "typescript": "^5.6.0"
8123
+ },
8124
+ "overrides": {
8125
+ "bedrock-agentcore": {
8126
+ "@strands-agents/sdk": "$@strands-agents/sdk"
8127
+ }
8128
+ }
8129
+ }
8130
+ "
8131
+ `;
8132
+
8133
+ exports[`Assets Directory Snapshots > TypeScript assets > typescript/typescript/http/strands/base/tsconfig.json should match snapshot 1`] = `
8134
+ "{
8135
+ "compilerOptions": {
8136
+ "target": "ES2022",
8137
+ "module": "NodeNext",
8138
+ "moduleResolution": "NodeNext",
8139
+ "strict": true,
8140
+ "esModuleInterop": true,
8141
+ "skipLibCheck": true,
8142
+ "forceConsistentCasingInFileNames": true,
8143
+ "resolveJsonModule": true,
8144
+ "declaration": false,
8145
+ "sourceMap": true,
8146
+ "outDir": "dist",
8147
+ "rootDir": ".",
8148
+ "types": ["node"]
8149
+ },
8150
+ "include": ["**/*.ts"],
8151
+ "exclude": ["node_modules", "dist"]
8152
+ }
8153
+ "
8154
+ `;
8155
+
8156
+ exports[`Assets Directory Snapshots > TypeScript assets > typescript/typescript/http/strands/capabilities/memory/memory.ts should match snapshot 1`] = `
8157
+ "import { randomUUID } from 'node:crypto';
8158
+ import { MemoryManager } from '@strands-agents/sdk';
8159
+ import { createAgentCoreMemoryStores } from 'bedrock-agentcore/experimental/memory/strands';
8160
+
8161
+ const MEMORY_ID = process.env.{{memoryProviders.[0].envVarName}};
8162
+
8163
+ const CUSTOM_ACTOR_ID_HEADER = 'x-amzn-bedrock-agentcore-runtime-custom-actor-id';
8164
+
8165
+ export function getActorId(payload: any, context: any): string {
8166
+ const raw =
8167
+ context?.headers?.[CUSTOM_ACTOR_ID_HEADER] ||
8168
+ payload?.userId ||
8169
+ context?.sessionId;
8170
+ return typeof raw === 'string' && raw.trim().length > 0 ? raw.trim() : randomUUID();
8171
+ }
8172
+
8173
+ const memoryManagerCache = new Map<string, MemoryManager>();
8174
+
8175
+ export function getOrCreateMemoryManager(sessionId: string, actorId: string): MemoryManager | null {
8176
+ if (!MEMORY_ID) return null;
8177
+
8178
+ const key = \`\${actorId}:\${sessionId}\`;
8179
+ let manager = memoryManagerCache.get(key);
8180
+ if (manager) return manager;
8181
+
8182
+ const stores = createAgentCoreMemoryStores({
8183
+ memoryId: MEMORY_ID,
8184
+ actorId,
8185
+ sessionId,
8186
+ namespaces: [
8187
+ {{#if (includes memoryProviders.[0].strategies "SEMANTIC")}}
8188
+ { namespace: '/users/{actorId}/facts' },
8189
+ {{/if}}
8190
+ {{#if (includes memoryProviders.[0].strategies "USER_PREFERENCE")}}
8191
+ { namespace: '/users/{actorId}/preferences' },
8192
+ {{/if}}
8193
+ {{#if (includes memoryProviders.[0].strategies "EPISODIC")}}
8194
+ { namespace: '/episodes/{actorId}/{sessionId}' },
8195
+ {{/if}}
8196
+ {{#if (includes memoryProviders.[0].strategies "SUMMARIZATION")}}
8197
+ { namespace: '/summaries/{actorId}/{sessionId}' },
8198
+ {{/if}}
8199
+ ],
8200
+ // readMode defaults to 'per-namespace' (one retrieve call per namespace).
8201
+ // Switch to 'subtree' to consolidate to a single hierarchical recall call.
8202
+ extraction: true,
8203
+ });
8204
+
8205
+ manager = new MemoryManager({ stores });
8206
+ memoryManagerCache.set(key, manager);
8207
+ return manager;
8208
+ }
8209
+ "
8210
+ `;
8211
+
8212
+ exports[`Assets Directory Snapshots > TypeScript assets > typescript/typescript/http/vercelai/base/README.md should match snapshot 1`] = `
8213
+ "This is a project generated by the AgentCore CLI!
8214
+
8215
+ # Layout
8216
+
8217
+ The generated application code lives at the agent root directory. At the root, there is a \`.gitignore\` file, an
8218
+ \`agentcore/\` folder which represents the configurations and state associated with this project. Other \`agentcore\`
8219
+ commands like \`deploy\`, \`dev\`, and \`invoke\` rely on the configuration stored here.
8220
+
8221
+ ## Agent Root
8222
+
8223
+ The main entrypoint to your app is defined in \`main.ts\`. Using the AgentCore SDK \`BedrockAgentCoreApp\`, this file
8224
+ defines an HTTP app that streams tokens using the Vercel AI SDK's \`streamText\` API.
8225
+
8226
+ \`model/load.ts\` instantiates your chosen model provider.
8227
+
8228
+ ## Input Validation
8229
+
8230
+ The generated Zod request schema keeps plain prompts typed as strings before forwarding them to the agent framework.
8231
+ Retain this validation when extending the request shape, and pass only prompt text to the agent.
8232
+
8233
+ ## Environment Variables
8234
+
8235
+ | Variable | Required | Description |
8236
+ | --- | --- | --- |
8237
+ {{#if hasIdentity}}| \`{{identityProviders.[0].envVarName}}\` | Yes | {{modelProvider}} API key (local) or Identity provider name (deployed) |
8238
+ {{/if}}| \`LOCAL_DEV\` | No | Set to \`1\` to use \`.env.local\` instead of AgentCore Identity |
8239
+
8240
+ # Developing locally
8241
+
8242
+ If installation was successful, \`node_modules/\` is already populated with dependencies.
8243
+
8244
+ \`agentcore dev\` will start a local server using \`npx tsx watch main.ts\` for hot reload. The port is logged to the terminal (default \`8080\`).
8245
+
8246
+ In a new terminal, you can invoke that server with:
8247
+
8248
+ \`agentcore invoke --dev "What can you do"\`
8249
+
8250
+ # Deployment
8251
+
8252
+ After providing credentials, \`agentcore deploy\` will deploy your project into Amazon Bedrock AgentCore.
8253
+
8254
+ Use \`agentcore invoke\` to invoke your deployed agent.
8255
+ "
8256
+ `;
8257
+
8258
+ exports[`Assets Directory Snapshots > TypeScript assets > typescript/typescript/http/vercelai/base/gitignore.template should match snapshot 1`] = `
8259
+ "# Environment variables
8260
+ .env
8261
+ .env.*
8262
+
8263
+ # Node
8264
+ node_modules/
8265
+ dist/
8266
+ *.log
8267
+ npm-debug.log*
8268
+ yarn-debug.log*
8269
+ yarn-error.log*
8270
+
8271
+ # IDE
8272
+ .vscode/
8273
+ .idea/
8274
+ *.swp
8275
+ *.swo
8276
+ *~
8277
+
8278
+ # OS
8279
+ .DS_Store
8280
+ Thumbs.db
8281
+ "
8282
+ `;
8283
+
8284
+ exports[`Assets Directory Snapshots > TypeScript assets > typescript/typescript/http/vercelai/base/main.ts should match snapshot 1`] = `
8285
+ "import { BedrockAgentCoreApp } from 'bedrock-agentcore/runtime';
8286
+ import { streamText, type ModelMessage } from 'ai';
8287
+ import { z } from 'zod';
8288
+ import { loadModel } from './model/load.js';
8289
+
8290
+ const SYSTEM_PROMPT = \`You are a helpful assistant.\`;
8291
+
8292
+ const requestSchema = z.object({
8293
+ prompt: z.string().default(''),
8294
+ });
8295
+
8296
+ const HISTORY_LIMIT = 128;
8297
+
8298
+ // Keeps one message history per sessionId so each session remembers its own
8299
+ // turns (best-effort; resets on cold start). A Map preserves insertion order,
8300
+ // so it doubles as an LRU bounded to 128 sessions — a local dev process serving
8301
+ // many sessions cannot leak history between them or grow without bound. On
8302
+ // AgentCore Runtime each microVM serves a single session, so this holds one
8303
+ // entry. For durable history, persist messages to an external store.
8304
+ const histories = new Map<string, ModelMessage[]>();
8305
+
8306
+ function getHistory(sessionId: string): ModelMessage[] {
8307
+ const existing = histories.get(sessionId);
8308
+ if (existing) {
8309
+ histories.delete(sessionId);
8310
+ histories.set(sessionId, existing);
8311
+ return existing;
8312
+ }
8313
+ if (histories.size >= HISTORY_LIMIT) {
8314
+ const oldest = histories.keys().next().value;
8315
+ if (oldest !== undefined) histories.delete(oldest);
8316
+ }
8317
+ const fresh: ModelMessage[] = [];
8318
+ histories.set(sessionId, fresh);
8319
+ return fresh;
8320
+ }
8321
+
8322
+ const app = new BedrockAgentCoreApp({
8323
+ invocationHandler: {
8324
+ requestSchema,
8325
+ async *process(payload, context) {
8326
+ const sessionId = context?.sessionId ?? 'default-session';
8327
+ const history = getHistory(sessionId);
8328
+ const userMessage: ModelMessage = { role: 'user', content: payload.prompt };
8329
+
8330
+ const model = await loadModel();
8331
+ const result = streamText({
8332
+ model,
8333
+ system: SYSTEM_PROMPT,
8334
+ messages: [...history, userMessage],
8335
+ });
8336
+
8337
+ let assistant = '';
8338
+ for await (const chunk of result.textStream) {
8339
+ assistant += chunk;
8340
+ yield { data: chunk };
8341
+ }
8342
+
8343
+ // Commit the exchange to history only after a non-empty reply. On a failed
8344
+ // or empty stream the turn is dropped instead of leaving a dangling user
8345
+ // (or empty assistant) message — consecutive same-role or empty-content
8346
+ // messages would otherwise be rejected on the next turn for this session.
8347
+ if (assistant.length > 0) {
8348
+ history.push(userMessage, { role: 'assistant', content: assistant });
8349
+ }
8350
+ },
8351
+ },
8352
+ });
8353
+
8354
+ app.run({ port: parseInt(process.env.PORT ?? '8080') });
8355
+ "
8356
+ `;
8357
+
8358
+ exports[`Assets Directory Snapshots > TypeScript assets > typescript/typescript/http/vercelai/base/model/load.ts should match snapshot 1`] = `
8359
+ "{{#if (eq modelProvider "Bedrock")}}
8360
+ import { createAmazonBedrock } from '@ai-sdk/amazon-bedrock';
8361
+ import { fromNodeProviderChain } from '@aws-sdk/credential-providers';
8362
+
8363
+ const provider = fromNodeProviderChain();
8364
+
8365
+ const bedrock = createAmazonBedrock({
8366
+ region: process.env.AWS_REGION ?? 'us-east-1',
8367
+ credentialProvider: async () => {
8368
+ const creds = await provider();
8369
+ return {
8370
+ accessKeyId: creds.accessKeyId,
8371
+ secretAccessKey: creds.secretAccessKey,
8372
+ sessionToken: creds.sessionToken,
8373
+ };
8374
+ },
8375
+ });
8376
+
8377
+ export function loadModel() {
8378
+ return bedrock('us.anthropic.claude-sonnet-4-5-20250929-v1:0');
8379
+ }
8380
+ {{/if}}
8381
+ {{#if (eq modelProvider "Anthropic")}}
8382
+ import { createAnthropic } from '@ai-sdk/anthropic';
8383
+ import { withApiKey } from 'bedrock-agentcore/identity';
8384
+
8385
+ const IDENTITY_PROVIDER_NAME = '{{identityProviders.[0].name}}';
8386
+ const IDENTITY_ENV_VAR = '{{identityProviders.[0].envVarName}}';
8387
+
8388
+ async function getApiKey(): Promise<string> {
8389
+ if (process.env.LOCAL_DEV === '1') {
8390
+ const apiKey = process.env[IDENTITY_ENV_VAR] ?? process.env.ANTHROPIC_API_KEY;
8391
+ if (!apiKey) {
8392
+ throw new Error(\`\${IDENTITY_ENV_VAR} or ANTHROPIC_API_KEY not found. Add your key to agentcore/.env.local\`);
8393
+ }
8394
+ return apiKey;
8395
+ }
8396
+ return withApiKey({ providerName: IDENTITY_PROVIDER_NAME })(async (apiKey: string) => apiKey)();
8397
+ }
8398
+
8399
+ let _anthropic: ReturnType<typeof createAnthropic> | undefined;
8400
+
8401
+ async function getProvider() {
8402
+ if (!_anthropic) {
8403
+ const apiKey = await getApiKey();
8404
+ _anthropic = createAnthropic({ apiKey });
8405
+ }
8406
+ return _anthropic;
8407
+ }
8408
+
8409
+ export async function loadModel() {
8410
+ const anthropic = await getProvider();
8411
+ return anthropic('claude-sonnet-4-5-20250929');
8412
+ }
8413
+ {{/if}}
8414
+ {{#if (eq modelProvider "OpenAI")}}
8415
+ import { createOpenAI } from '@ai-sdk/openai';
8416
+ import { withApiKey } from 'bedrock-agentcore/identity';
8417
+
8418
+ const IDENTITY_PROVIDER_NAME = '{{identityProviders.[0].name}}';
8419
+ const IDENTITY_ENV_VAR = '{{identityProviders.[0].envVarName}}';
8420
+
8421
+ async function getApiKey(): Promise<string> {
8422
+ if (process.env.LOCAL_DEV === '1') {
8423
+ const apiKey = process.env[IDENTITY_ENV_VAR] ?? process.env.OPENAI_API_KEY;
8424
+ if (!apiKey) {
8425
+ throw new Error(\`\${IDENTITY_ENV_VAR} or OPENAI_API_KEY not found. Add your key to agentcore/.env.local\`);
8426
+ }
8427
+ return apiKey;
8428
+ }
8429
+ return withApiKey({ providerName: IDENTITY_PROVIDER_NAME })(async (apiKey: string) => apiKey)();
8430
+ }
8431
+
8432
+ let _openai: ReturnType<typeof createOpenAI> | undefined;
8433
+
8434
+ async function getProvider() {
8435
+ if (!_openai) {
8436
+ const apiKey = await getApiKey();
8437
+ _openai = createOpenAI({ apiKey });
8438
+ }
8439
+ return _openai;
8440
+ }
8441
+
8442
+ export async function loadModel() {
8443
+ const openai = await getProvider();
8444
+ return openai('gpt-4.1');
8445
+ }
8446
+ {{/if}}
8447
+ {{#if (eq modelProvider "Gemini")}}
8448
+ import { createGoogleGenerativeAI } from '@ai-sdk/google';
8449
+ import { withApiKey } from 'bedrock-agentcore/identity';
8450
+
8451
+ const IDENTITY_PROVIDER_NAME = '{{identityProviders.[0].name}}';
8452
+ const IDENTITY_ENV_VAR = '{{identityProviders.[0].envVarName}}';
8453
+
8454
+ async function getApiKey(): Promise<string> {
8455
+ if (process.env.LOCAL_DEV === '1') {
8456
+ const apiKey = process.env[IDENTITY_ENV_VAR] ?? process.env.GEMINI_API_KEY;
8457
+ if (!apiKey) {
8458
+ throw new Error(\`\${IDENTITY_ENV_VAR} or GEMINI_API_KEY not found. Add your key to agentcore/.env.local\`);
8459
+ }
8460
+ return apiKey;
8461
+ }
8462
+ return withApiKey({ providerName: IDENTITY_PROVIDER_NAME })(async (apiKey: string) => apiKey)();
8463
+ }
8464
+
8465
+ let _google: ReturnType<typeof createGoogleGenerativeAI> | undefined;
8466
+
8467
+ async function getProvider() {
8468
+ if (!_google) {
8469
+ const apiKey = await getApiKey();
8470
+ _google = createGoogleGenerativeAI({ apiKey });
8471
+ }
8472
+ return _google;
8473
+ }
8474
+
8475
+ export async function loadModel() {
8476
+ const google = await getProvider();
8477
+ return google('gemini-2.5-flash');
8478
+ }
8479
+ {{/if}}
8480
+ "
8481
+ `;
8482
+
8483
+ exports[`Assets Directory Snapshots > TypeScript assets > typescript/typescript/http/vercelai/base/package.json should match snapshot 1`] = `
8484
+ "{
8485
+ "name": "{{name}}",
8486
+ "version": "0.1.0",
8487
+ "description": "AgentCore Runtime Application using Vercel AI SDK",
8488
+ "private": true,
8489
+ "type": "module",
8490
+ "scripts": {
8491
+ "build": "tsc",
8492
+ "start": "node dist/main.js",
8493
+ "dev": "tsx watch main.ts"
8494
+ },
8495
+ "dependencies": {
8496
+ "ai": "^6.0.0",
8497
+ {{#if (eq modelProvider "Bedrock")}}
8498
+ "@ai-sdk/amazon-bedrock": "^4.0.0",
8499
+ "@aws-sdk/credential-providers": "^3.0.0",
8500
+ {{/if}}
8501
+ {{#if (eq modelProvider "Anthropic")}}
8502
+ "@ai-sdk/anthropic": "^3.0.0",
8503
+ {{/if}}
8504
+ {{#if (eq modelProvider "OpenAI")}}
8505
+ "@ai-sdk/openai": "^3.0.0",
8506
+ {{/if}}
8507
+ {{#if (eq modelProvider "Gemini")}}
8508
+ "@ai-sdk/google": "^3.0.0",
8509
+ {{/if}}
8510
+ "bedrock-agentcore": "^0.2.4",
8511
+ "tsx": "^4.19.0",
8512
+ "zod": "^3.24.0"
8513
+ },
8514
+ "devDependencies": {
8515
+ "@types/node": "^22.0.0",
8516
+ "typescript": "^5.6.0"
8517
+ }
8518
+ }
8519
+ "
8520
+ `;
8521
+
8522
+ exports[`Assets Directory Snapshots > TypeScript assets > typescript/typescript/http/vercelai/base/tsconfig.json should match snapshot 1`] = `
8523
+ "{
8524
+ "compilerOptions": {
8525
+ "target": "ES2022",
8526
+ "module": "NodeNext",
8527
+ "moduleResolution": "NodeNext",
8528
+ "strict": true,
8529
+ "esModuleInterop": true,
8530
+ "skipLibCheck": true,
8531
+ "forceConsistentCasingInFileNames": true,
8532
+ "resolveJsonModule": true,
8533
+ "declaration": false,
8534
+ "sourceMap": true,
8535
+ "outDir": "dist",
8536
+ "rootDir": ".",
8537
+ "types": ["node"]
8538
+ },
8539
+ "include": ["**/*.ts"],
8540
+ "exclude": ["node_modules", "dist"]
8541
+ }
8542
+ "
8543
+ `;