@aws/agentcore 1.0.0-preview.2 → 1.0.0-preview.20

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 (256) 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/__tests__/__snapshots__/assets.snapshot.test.ts.snap +3132 -366
  5. package/dist/assets/__tests__/__snapshots__/dockerfile-render.test.ts.snap +6 -2
  6. package/dist/assets/__tests__/assets.snapshot.test.ts +10 -24
  7. package/dist/assets/__tests__/googleadk-session-eviction.test.ts +87 -0
  8. package/dist/assets/__tests__/summarization-namespace.test.ts +43 -0
  9. package/dist/assets/agents/AGENTS.md +87 -11
  10. package/dist/assets/cdk/bin/cdk.ts +87 -26
  11. package/dist/assets/cdk/lib/cdk-stack.ts +185 -55
  12. package/dist/assets/cdk/package.json +1 -1
  13. package/dist/assets/cdk/test/cdk.test.ts +4 -0
  14. package/dist/assets/container/python/Dockerfile +7 -1
  15. package/dist/assets/container/typescript/Dockerfile +25 -0
  16. package/dist/assets/container/typescript/dockerignore.template +24 -0
  17. package/dist/assets/datasets/predefined-v1.jsonl +3 -0
  18. package/dist/assets/datasets/simulated-v1.jsonl +3 -0
  19. package/dist/assets/python/a2a/googleadk/base/main.py +24 -16
  20. package/dist/assets/python/a2a/googleadk/base/pyproject.toml +3 -3
  21. package/dist/assets/python/a2a/langchain_langgraph/base/main.py +24 -16
  22. package/dist/assets/python/a2a/langchain_langgraph/base/pyproject.toml +3 -2
  23. package/dist/assets/python/a2a/strands/base/main.py +23 -17
  24. package/dist/assets/python/a2a/strands/base/pyproject.toml +2 -2
  25. package/dist/assets/python/a2a/strands/capabilities/memory/session.py +10 -2
  26. package/dist/assets/python/agui/googleadk/base/main.py +62 -2
  27. package/dist/assets/python/agui/googleadk/base/pyproject.toml +3 -3
  28. package/dist/assets/python/agui/langchain_langgraph/base/main.py +56 -0
  29. package/dist/assets/python/agui/langchain_langgraph/base/pyproject.toml +2 -1
  30. package/dist/assets/python/agui/strands/base/main.py +63 -1
  31. package/dist/assets/python/agui/strands/base/pyproject.toml +1 -1
  32. package/dist/assets/python/agui/strands/capabilities/memory/session.py +10 -2
  33. package/dist/assets/python/http/autogen/base/main.py +60 -29
  34. package/dist/assets/python/http/autogen/base/pyproject.toml +1 -1
  35. package/dist/assets/python/http/googleadk/base/main.py +69 -24
  36. package/dist/assets/python/http/googleadk/base/mcp_client/client.py +7 -7
  37. package/dist/assets/python/http/googleadk/base/pyproject.toml +2 -1
  38. package/dist/assets/python/http/langchain_langgraph/base/main.py +115 -24
  39. package/dist/assets/python/http/langchain_langgraph/base/mcp_client/client.py +10 -8
  40. package/dist/assets/python/http/langchain_langgraph/base/pyproject.toml +3 -2
  41. package/dist/assets/python/http/openaiagents/base/main.py +61 -32
  42. package/dist/assets/python/http/openaiagents/base/mcp_client/client.py +4 -4
  43. package/dist/assets/python/http/openaiagents/base/pyproject.toml +2 -2
  44. package/dist/assets/python/http/strands/base/main.py +571 -47
  45. package/dist/assets/python/http/strands/base/mcp_client/client.py +49 -6
  46. package/dist/assets/python/http/strands/base/model/load.py +119 -3
  47. package/dist/assets/python/http/strands/base/model/mantle_compat.py +21 -0
  48. package/dist/assets/python/http/strands/base/pyproject.toml +9 -3
  49. package/dist/assets/python/http/strands/base/skills/fetcher.py +279 -0
  50. package/dist/assets/python/http/strands/capabilities/execution-limits/hooks/execution_limits.py +54 -0
  51. package/dist/assets/python/http/strands/capabilities/memory/session.py +10 -2
  52. package/dist/assets/python/http/strands/capabilities/payments/__init__.py +4 -0
  53. package/dist/assets/python/http/strands/capabilities/payments/payments.py +142 -0
  54. package/dist/assets/python/mcp/standalone/base/main.py +58 -0
  55. package/dist/assets/typescript/http/strands/base/README.md +37 -0
  56. package/dist/assets/typescript/http/strands/base/gitignore.template +22 -0
  57. package/dist/assets/typescript/http/strands/base/main.ts +144 -0
  58. package/dist/assets/typescript/http/strands/base/mcp_client/client.ts +11 -0
  59. package/dist/assets/typescript/http/strands/base/model/load.ts +102 -0
  60. package/dist/assets/typescript/http/strands/base/package.json +38 -0
  61. package/dist/assets/typescript/http/strands/base/tsconfig.json +19 -0
  62. package/dist/assets/typescript/http/strands/capabilities/memory/memory.ts +52 -0
  63. package/dist/assets/typescript/http/vercelai/base/README.md +37 -0
  64. package/dist/assets/typescript/http/vercelai/base/gitignore.template +22 -0
  65. package/dist/assets/typescript/http/vercelai/base/main.ts +64 -0
  66. package/dist/assets/typescript/http/vercelai/base/model/load.ts +121 -0
  67. package/dist/assets/typescript/http/vercelai/base/package.json +35 -0
  68. package/dist/assets/typescript/http/vercelai/base/tsconfig.json +19 -0
  69. package/dist/cli/index.mjs +1416 -568
  70. package/dist/lib/errors/index.d.ts +1 -1
  71. package/dist/lib/errors/index.d.ts.map +1 -1
  72. package/dist/lib/errors/index.js +1 -1
  73. package/dist/lib/errors/index.js.map +1 -1
  74. package/dist/lib/errors/types.d.ts +259 -0
  75. package/dist/lib/errors/types.d.ts.map +1 -0
  76. package/dist/lib/errors/types.js +402 -0
  77. package/dist/lib/errors/types.js.map +1 -0
  78. package/dist/lib/errors/zod.d.ts +8 -0
  79. package/dist/lib/errors/zod.d.ts.map +1 -0
  80. package/dist/lib/errors/{config.js → zod.js} +8 -81
  81. package/dist/lib/errors/zod.js.map +1 -0
  82. package/dist/lib/index.d.ts +3 -0
  83. package/dist/lib/index.d.ts.map +1 -1
  84. package/dist/lib/index.js +4 -1
  85. package/dist/lib/index.js.map +1 -1
  86. package/dist/lib/packaging/build-args.js +2 -2
  87. package/dist/lib/packaging/build-args.js.map +1 -1
  88. package/dist/lib/packaging/container.js +5 -5
  89. package/dist/lib/packaging/container.js.map +1 -1
  90. package/dist/lib/packaging/helpers.d.ts +16 -0
  91. package/dist/lib/packaging/helpers.d.ts.map +1 -1
  92. package/dist/lib/packaging/helpers.js +127 -29
  93. package/dist/lib/packaging/helpers.js.map +1 -1
  94. package/dist/lib/packaging/index.d.ts +0 -1
  95. package/dist/lib/packaging/index.d.ts.map +1 -1
  96. package/dist/lib/packaging/index.js +4 -19
  97. package/dist/lib/packaging/index.js.map +1 -1
  98. package/dist/lib/packaging/node.d.ts +2 -2
  99. package/dist/lib/packaging/node.d.ts.map +1 -1
  100. package/dist/lib/packaging/node.js +83 -38
  101. package/dist/lib/packaging/node.js.map +1 -1
  102. package/dist/lib/packaging/python.js +12 -12
  103. package/dist/lib/packaging/python.js.map +1 -1
  104. package/dist/lib/result.d.ts +50 -0
  105. package/dist/lib/result.d.ts.map +1 -0
  106. package/dist/lib/result.js +49 -0
  107. package/dist/lib/result.js.map +1 -0
  108. package/dist/lib/schemas/io/config-io.d.ts +8 -0
  109. package/dist/lib/schemas/io/config-io.d.ts.map +1 -1
  110. package/dist/lib/schemas/io/config-io.js +21 -3
  111. package/dist/lib/schemas/io/config-io.js.map +1 -1
  112. package/dist/lib/schemas/io/global-config.d.ts +40 -0
  113. package/dist/lib/schemas/io/global-config.d.ts.map +1 -0
  114. package/dist/lib/schemas/io/global-config.js +118 -0
  115. package/dist/lib/schemas/io/global-config.js.map +1 -0
  116. package/dist/lib/schemas/io/index.d.ts +1 -2
  117. package/dist/lib/schemas/io/index.d.ts.map +1 -1
  118. package/dist/lib/schemas/io/index.js +1 -4
  119. package/dist/lib/schemas/io/index.js.map +1 -1
  120. package/dist/lib/schemas/io/path-resolver.d.ts +0 -6
  121. package/dist/lib/schemas/io/path-resolver.d.ts.map +1 -1
  122. package/dist/lib/schemas/io/path-resolver.js +3 -13
  123. package/dist/lib/schemas/io/path-resolver.js.map +1 -1
  124. package/dist/lib/secrets/cipher.d.ts +12 -0
  125. package/dist/lib/secrets/cipher.d.ts.map +1 -0
  126. package/dist/lib/secrets/cipher.js +54 -0
  127. package/dist/lib/secrets/cipher.js.map +1 -0
  128. package/dist/lib/secrets/index.d.ts +4 -0
  129. package/dist/lib/secrets/index.d.ts.map +1 -0
  130. package/dist/lib/secrets/index.js +15 -0
  131. package/dist/lib/secrets/index.js.map +1 -0
  132. package/dist/lib/secrets/key-provider.d.ts +16 -0
  133. package/dist/lib/secrets/key-provider.d.ts.map +1 -0
  134. package/dist/lib/secrets/key-provider.js +191 -0
  135. package/dist/lib/secrets/key-provider.js.map +1 -0
  136. package/dist/lib/secrets/sensitive-keys.d.ts +21 -0
  137. package/dist/lib/secrets/sensitive-keys.d.ts.map +1 -0
  138. package/dist/lib/secrets/sensitive-keys.js +67 -0
  139. package/dist/lib/secrets/sensitive-keys.js.map +1 -0
  140. package/dist/lib/time-constants.d.ts +5 -0
  141. package/dist/lib/time-constants.d.ts.map +1 -0
  142. package/dist/lib/time-constants.js +8 -0
  143. package/dist/lib/time-constants.js.map +1 -0
  144. package/dist/lib/utils/env.d.ts +8 -2
  145. package/dist/lib/utils/env.d.ts.map +1 -1
  146. package/dist/lib/utils/env.js +65 -17
  147. package/dist/lib/utils/env.js.map +1 -1
  148. package/dist/lib/utils/index.d.ts +3 -2
  149. package/dist/lib/utils/index.d.ts.map +1 -1
  150. package/dist/lib/utils/index.js +6 -1
  151. package/dist/lib/utils/index.js.map +1 -1
  152. package/dist/lib/utils/polling.d.ts +31 -0
  153. package/dist/lib/utils/polling.d.ts.map +1 -0
  154. package/dist/lib/utils/polling.js +61 -0
  155. package/dist/lib/utils/polling.js.map +1 -0
  156. package/dist/lib/utils/zod.d.ts +16 -0
  157. package/dist/lib/utils/zod.d.ts.map +1 -1
  158. package/dist/lib/utils/zod.js +45 -0
  159. package/dist/lib/utils/zod.js.map +1 -1
  160. package/dist/schema/constants.d.ts +56 -2
  161. package/dist/schema/constants.d.ts.map +1 -1
  162. package/dist/schema/constants.js +82 -7
  163. package/dist/schema/constants.js.map +1 -1
  164. package/dist/schema/schemas/agent-env.d.ts +156 -6
  165. package/dist/schema/schemas/agent-env.d.ts.map +1 -1
  166. package/dist/schema/schemas/agent-env.js +186 -13
  167. package/dist/schema/schemas/agent-env.js.map +1 -1
  168. package/dist/schema/schemas/agentcore-project.d.ts +446 -11
  169. package/dist/schema/schemas/agentcore-project.d.ts.map +1 -1
  170. package/dist/schema/schemas/agentcore-project.js +283 -18
  171. package/dist/schema/schemas/agentcore-project.js.map +1 -1
  172. package/dist/schema/schemas/auth.d.ts +174 -0
  173. package/dist/schema/schemas/auth.d.ts.map +1 -1
  174. package/dist/schema/schemas/auth.js +127 -3
  175. package/dist/schema/schemas/auth.js.map +1 -1
  176. package/dist/schema/schemas/connections.d.ts +185 -0
  177. package/dist/schema/schemas/connections.d.ts.map +1 -0
  178. package/dist/schema/schemas/connections.js +176 -0
  179. package/dist/schema/schemas/connections.js.map +1 -0
  180. package/dist/schema/schemas/deployed-state.d.ts +361 -5
  181. package/dist/schema/schemas/deployed-state.d.ts.map +1 -1
  182. package/dist/schema/schemas/deployed-state.js +105 -2
  183. package/dist/schema/schemas/deployed-state.js.map +1 -1
  184. package/dist/schema/schemas/index.d.ts +1 -0
  185. package/dist/schema/schemas/index.d.ts.map +1 -1
  186. package/dist/schema/schemas/index.js +1 -0
  187. package/dist/schema/schemas/index.js.map +1 -1
  188. package/dist/schema/schemas/mcp-defs.d.ts +2 -1
  189. package/dist/schema/schemas/mcp-defs.d.ts.map +1 -1
  190. package/dist/schema/schemas/mcp-defs.js +3 -2
  191. package/dist/schema/schemas/mcp-defs.js.map +1 -1
  192. package/dist/schema/schemas/mcp.d.ts +197 -0
  193. package/dist/schema/schemas/mcp.d.ts.map +1 -1
  194. package/dist/schema/schemas/mcp.js +342 -6
  195. package/dist/schema/schemas/mcp.js.map +1 -1
  196. package/dist/schema/schemas/primitives/ab-test.d.ts +135 -0
  197. package/dist/schema/schemas/primitives/ab-test.d.ts.map +1 -0
  198. package/dist/schema/schemas/primitives/ab-test.js +95 -0
  199. package/dist/schema/schemas/primitives/ab-test.js.map +1 -0
  200. package/dist/schema/schemas/primitives/config-bundle.d.ts +32 -0
  201. package/dist/schema/schemas/primitives/config-bundle.d.ts.map +1 -0
  202. package/dist/schema/schemas/primitives/config-bundle.js +41 -0
  203. package/dist/schema/schemas/primitives/config-bundle.js.map +1 -0
  204. package/dist/schema/schemas/primitives/dataset.d.ts +48 -0
  205. package/dist/schema/schemas/primitives/dataset.d.ts.map +1 -0
  206. package/dist/schema/schemas/primitives/dataset.js +58 -0
  207. package/dist/schema/schemas/primitives/dataset.js.map +1 -0
  208. package/dist/schema/schemas/primitives/evaluator.d.ts +8 -0
  209. package/dist/schema/schemas/primitives/evaluator.d.ts.map +1 -1
  210. package/dist/schema/schemas/primitives/evaluator.js +17 -1
  211. package/dist/schema/schemas/primitives/evaluator.js.map +1 -1
  212. package/dist/schema/schemas/primitives/harness.d.ts +377 -19
  213. package/dist/schema/schemas/primitives/harness.d.ts.map +1 -1
  214. package/dist/schema/schemas/primitives/harness.js +379 -24
  215. package/dist/schema/schemas/primitives/harness.js.map +1 -1
  216. package/dist/schema/schemas/primitives/index.d.ts +13 -7
  217. package/dist/schema/schemas/primitives/index.d.ts.map +1 -1
  218. package/dist/schema/schemas/primitives/index.js +37 -1
  219. package/dist/schema/schemas/primitives/index.js.map +1 -1
  220. package/dist/schema/schemas/primitives/knowledge-base.d.ts +104 -0
  221. package/dist/schema/schemas/primitives/knowledge-base.d.ts.map +1 -0
  222. package/dist/schema/schemas/primitives/knowledge-base.js +99 -0
  223. package/dist/schema/schemas/primitives/knowledge-base.js.map +1 -0
  224. package/dist/schema/schemas/primitives/memory.d.ts +22 -4
  225. package/dist/schema/schemas/primitives/memory.d.ts.map +1 -1
  226. package/dist/schema/schemas/primitives/memory.js +57 -16
  227. package/dist/schema/schemas/primitives/memory.js.map +1 -1
  228. package/dist/schema/schemas/primitives/online-eval-config.d.ts +21 -2
  229. package/dist/schema/schemas/primitives/online-eval-config.d.ts.map +1 -1
  230. package/dist/schema/schemas/primitives/online-eval-config.js +45 -5
  231. package/dist/schema/schemas/primitives/online-eval-config.js.map +1 -1
  232. package/dist/schema/schemas/primitives/payment.d.ts +54 -0
  233. package/dist/schema/schemas/primitives/payment.d.ts.map +1 -0
  234. package/dist/schema/schemas/primitives/payment.js +79 -0
  235. package/dist/schema/schemas/primitives/payment.js.map +1 -0
  236. package/dist/schema/schemas/primitives/policy.d.ts +26 -0
  237. package/dist/schema/schemas/primitives/policy.d.ts.map +1 -1
  238. package/dist/schema/schemas/primitives/policy.js +5 -1
  239. package/dist/schema/schemas/primitives/policy.js.map +1 -1
  240. package/npm-shrinkwrap.json +17013 -0
  241. package/package.json +43 -21
  242. package/scripts/bump-version.ts +4 -4
  243. package/scripts/bundle.mjs +70 -41
  244. package/scripts/run-e2e-dev.sh +56 -0
  245. package/scripts/run-e2e-local.sh +145 -0
  246. package/dist/lib/errors/config.d.ts +0 -49
  247. package/dist/lib/errors/config.d.ts.map +0 -1
  248. package/dist/lib/errors/config.js.map +0 -1
  249. package/dist/lib/packaging/errors.d.ts +0 -16
  250. package/dist/lib/packaging/errors.d.ts.map +0 -1
  251. package/dist/lib/packaging/errors.js +0 -36
  252. package/dist/lib/packaging/errors.js.map +0 -1
  253. package/dist/lib/schemas/io/cli-config.d.ts +0 -12
  254. package/dist/lib/schemas/io/cli-config.d.ts.map +0 -1
  255. package/dist/lib/schemas/io/cli-config.js +0 -35
  256. 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
- dockerfileName?: string;
114
- harnessDir?: 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
- dockerfileName: harnessSpec.dockerfile,
130
- harnessDir,
142
+ dockerfile: harnessSpec.dockerfile,
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,52 @@ 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: { name: string; provider?: string; credentialName: string }[];
190
+ }) => ({
191
+ name: p.name,
192
+ description: p.description,
193
+ authorizerType: p.authorizerType,
194
+ authorizerConfiguration: p.authorizerConfiguration,
195
+ autoPayment: p.autoPayment,
196
+ paymentToolAllowlist: p.paymentToolAllowlist,
197
+ networkPreferences: p.networkPreferences,
198
+ connectors: p.connectors.map(c => {
199
+ const credentialProviderArn = paymentCredentials?.[c.credentialName]?.credentialProviderArn;
200
+ if (!credentialProviderArn) {
201
+ // Fail fast with an actionable message rather than passing an empty
202
+ // ARN that fails opaquely server-side at CreatePaymentConnector.
203
+ throw new Error(
204
+ \`Payment connector "\${c.name}" on manager "\${p.name}" references credential \` +
205
+ \`"\${c.credentialName}", but no deployed credential provider was found for it. \` +
206
+ \`Run \\\`agentcore deploy\\\` so the credential provider is created first.\`
207
+ );
208
+ }
209
+ return { name: c.name, provider: c.provider, credentialProviderArn };
210
+ }),
211
+ })
212
+ )
213
+ : undefined;
214
+
156
215
  new AgentCoreStack(app, stackName, {
157
216
  spec,
158
217
  mcpSpec,
159
218
  credentials,
219
+ connectorParametersByFile,
160
220
  harnesses: harnessConfigs.length > 0 ? harnessConfigs : undefined,
221
+ paymentSpec,
161
222
  env,
162
223
  description: \`AgentCore stack for \${spec.name} deployed to \${target.name} (\${target.region})\`,
163
224
  tags: {
@@ -172,7 +233,7 @@ async function main() {
172
233
 
173
234
  main().catch((error: unknown) => {
174
235
  console.error('AgentCore CDK synthesis failed:', error instanceof Error ? error.message : error);
175
- process.exitCode = 1;
236
+ process.exit(1);
176
237
  });
177
238
  "
178
239
  `;
@@ -299,26 +360,39 @@ exports[`Assets Directory Snapshots > CDK assets > cdk/cdk/lib/cdk-stack.ts shou
299
360
  "import {
300
361
  AgentCoreApplication,
301
362
  AgentCoreMcp,
363
+ AgentCorePaymentManager,
364
+ AgentCorePaymentConnector,
302
365
  type AgentCoreProjectSpec,
303
366
  type AgentCoreMcpSpec,
304
- ContainerSourceAssetFromPath,
305
- AgentEcrRepository,
306
- ContainerBuildProject,
307
- ContainerImageBuilder,
367
+ type CustomJWTAuthorizerConfig,
368
+ type HarnessDeploymentConfig,
308
369
  } from '@aws/agentcore-cdk';
309
370
  import { CfnOutput, Stack, type StackProps } from 'aws-cdk-lib';
371
+ import * as iam from 'aws-cdk-lib/aws-iam';
310
372
  import { Construct } from 'constructs';
311
373
 
312
- export interface HarnessConfig {
374
+ /**
375
+ * Harness deployment config: role-scoped fields (for IAM role + container build)
376
+ * plus the full validated spec + its config directory so the L3 construct can
377
+ * synthesize the AWS::BedrockAgentCore::Harness resource.
378
+ */
379
+ export type HarnessConfig = HarnessDeploymentConfig;
380
+
381
+ export interface PaymentConnectorSpec {
382
+ name: string;
383
+ provider: 'CoinbaseCDP' | 'StripePrivy';
384
+ credentialProviderArn: string;
385
+ }
386
+
387
+ export interface PaymentSpec {
313
388
  name: string;
314
- executionRoleArn?: string;
315
- memoryName?: string;
316
- containerUri?: string;
317
- hasDockerfile?: boolean;
318
- dockerfileName?: string;
319
- harnessDir?: string;
320
- tools?: { type: string; name: string }[];
321
- apiKeyArn?: string;
389
+ description?: string;
390
+ authorizerType: 'AWS_IAM' | 'CUSTOM_JWT';
391
+ authorizerConfiguration?: { customJWTAuthorizer: CustomJWTAuthorizerConfig };
392
+ autoPayment?: boolean;
393
+ paymentToolAllowlist?: string[];
394
+ networkPreferences?: string[];
395
+ connectors: PaymentConnectorSpec[];
322
396
  }
323
397
 
324
398
  export interface AgentCoreStackProps extends StackProps {
@@ -335,9 +409,38 @@ export interface AgentCoreStackProps extends StackProps {
335
409
  */
336
410
  credentials?: Record<string, { credentialProviderArn: string; clientSecretArn?: string }>;
337
411
  /**
338
- * Harness role configurations. Each entry creates an IAM execution role for a harness.
412
+ * Harness role configurations.
339
413
  */
340
414
  harnesses?: HarnessConfig[];
415
+ /**
416
+ * Parsed connectorParameters for non-S3 KB data sources, keyed by
417
+ * connectorConfigFile path. Forwarded to AgentCoreApplication.
418
+ */
419
+ connectorParametersByFile?: Record<string, Record<string, unknown>>;
420
+ /**
421
+ * Payment specifications with resolved credential provider ARNs.
422
+ */
423
+ paymentSpec?: PaymentSpec[];
424
+ }
425
+
426
+ function toCdkId(name: string): string {
427
+ return name.replace(/_/g, '');
428
+ }
429
+
430
+ /**
431
+ * Decide whether a deployed runtime should receive payment env vars + IAM grants.
432
+ * Payments today only ships a runtime shim for Python HTTP runtimes; injecting
433
+ * AGENTCORE_PAYMENT_* env vars into TypeScript / MCP / A2A / AGUI runtimes
434
+ * would surface env vars they cannot consume and would dilute least-privilege
435
+ * IAM grants for runtimes that never call ProcessPayment.
436
+ */
437
+ function isPaymentEligibleAgent(agent: { entrypoint?: string; protocol?: string }): boolean {
438
+ if (agent.protocol && agent.protocol !== 'HTTP') {
439
+ return false;
440
+ }
441
+ const entrypoint = typeof agent.entrypoint === 'string' ? agent.entrypoint : '';
442
+ const entrypointFile = entrypoint.split(':')[0] ?? '';
443
+ return entrypointFile.endsWith('.py');
341
444
  }
342
445
 
343
446
  /**
@@ -353,49 +456,21 @@ export class AgentCoreStack extends Stack {
353
456
  constructor(scope: Construct, id: string, props: AgentCoreStackProps) {
354
457
  super(scope, id, props);
355
458
 
356
- const { spec, mcpSpec, credentials, harnesses } = props;
357
-
358
- // Build container images for harnesses that specify a dockerfile (no containerUri).
359
- // Produces CDK outputs consumed by the imperative harness deployer.
360
- const harnessesForCdk = harnesses ? [...harnesses] : [];
361
- if (harnesses) {
362
- for (let i = 0; i < harnesses.length; i++) {
363
- const h = harnesses[i]!;
364
- if (h.hasDockerfile && !h.containerUri && h.harnessDir) {
365
- const pascalName = h.name.replace(/(^|_)([a-z])/g, (_: string, __: string, c: string) => c.toUpperCase());
366
- const sourceAsset = new ContainerSourceAssetFromPath(this, \`Harness\${pascalName}SourceAsset\`, {
367
- sourcePath: h.harnessDir,
368
- });
369
- const ecrRepo = new AgentEcrRepository(this, \`Harness\${pascalName}EcrRepo\`, {
370
- projectName: spec.name,
371
- agentName: \`harness-\${h.name}\`,
372
- });
373
- const buildProject = ContainerBuildProject.getOrCreate(this);
374
- buildProject.grantPushTo(ecrRepo.repository);
375
- sourceAsset.asset.grantRead(buildProject.role);
376
-
377
- const builder = new ContainerImageBuilder(this, \`Harness\${pascalName}ContainerBuild\`, {
378
- buildProject,
379
- sourceAsset,
380
- repository: ecrRepo,
381
- dockerfile: h.dockerfileName ?? 'Dockerfile',
382
- });
383
-
384
- new CfnOutput(this, \`Harness\${pascalName}ContainerUriOutput\`, {
385
- value: builder.containerUri,
386
- });
387
-
388
- // Pass the built containerUri to the harness role construct so it gets ECR pull permissions
389
- harnessesForCdk[i] = { ...h, containerUri: builder.containerUri };
390
- }
391
- }
392
- }
459
+ const { spec, mcpSpec, credentials, harnesses, connectorParametersByFile, paymentSpec } = props;
393
460
 
394
461
  // Create AgentCoreApplication with all agents and harness roles
395
- this.application = new AgentCoreApplication(this, 'Application', {
396
- spec,
397
- harnesses: harnessesForCdk.length > 0 ? harnessesForCdk : undefined,
398
- });
462
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
463
+ const appProps: Record<string, unknown> = { spec };
464
+ if (harnesses?.length) {
465
+ appProps.harnesses = harnesses;
466
+ }
467
+ if (connectorParametersByFile && Object.keys(connectorParametersByFile).length > 0) {
468
+ appProps.connectorParametersByFile = connectorParametersByFile;
469
+ }
470
+ if (credentials) {
471
+ appProps.credentials = credentials;
472
+ }
473
+ this.application = new AgentCoreApplication(this, 'Application', appProps as any);
399
474
 
400
475
  // Create AgentCoreMcp if there are gateways configured
401
476
  if (mcpSpec?.agentCoreGateways && mcpSpec.agentCoreGateways.length > 0) {
@@ -408,6 +483,122 @@ export class AgentCoreStack extends Stack {
408
483
  });
409
484
  }
410
485
 
486
+ // Create payment infrastructure via CFN constructs
487
+ if (paymentSpec && paymentSpec.length > 0) {
488
+ for (const payment of paymentSpec) {
489
+ const mgrId = toCdkId(payment.name);
490
+ const manager = new AgentCorePaymentManager(this, \`Payment\${mgrId}\`, {
491
+ projectName: spec.name,
492
+ name: payment.name,
493
+ authorizerType: payment.authorizerType,
494
+ description: payment.description,
495
+ authorizerConfiguration: payment.authorizerConfiguration,
496
+ tags: spec.tags,
497
+ });
498
+
499
+ const prefix = \`AGENTCORE_PAYMENT_\${payment.name.toUpperCase().replace(/-/g, '_')}\`;
500
+
501
+ // Wire env vars from construct output tokens into eligible agent environments only.
502
+ // See isPaymentEligibleAgent — non-Python or non-HTTP runtimes have no shim that
503
+ // can consume these env vars, and giving them sts:AssumeRole on the
504
+ // ProcessPaymentRole would broaden the privilege surface unnecessarily.
505
+ for (const env of this.application.environments.values()) {
506
+ if (!isPaymentEligibleAgent(env.agent)) {
507
+ continue;
508
+ }
509
+ env.runtime.addEnvironmentVariable(\`\${prefix}_MANAGER_ARN\`, manager.paymentManagerArn);
510
+ env.runtime.addEnvironmentVariable(\`\${prefix}_PROCESS_PAYMENT_ROLE_ARN\`, manager.processPaymentRoleArn);
511
+
512
+ // Grant runtime execution role permission to assume the ProcessPaymentRole.
513
+ // The ProcessPaymentRole's trust policy allows AccountRootPrincipal, but the
514
+ // caller still needs sts:AssumeRole on its own role to perform the assumption.
515
+ env.runtime.role.addToPrincipalPolicy(
516
+ new iam.PolicyStatement({
517
+ actions: ['sts:AssumeRole'],
518
+ resources: [manager.processPaymentRoleArn],
519
+ })
520
+ );
521
+
522
+ // Grant payment data-plane actions directly to the runtime role.
523
+ //
524
+ // NOTE: This deviates from the canonical role model in the AgentCore Payments
525
+ // beta guide, which assigns Get/List/Create instrument+session actions to a
526
+ // separate ManagementRole and limits the agent's role to ProcessPayment only.
527
+ // The current SDK plugin (AgentCorePaymentsPlugin.generate_payment_header)
528
+ // calls GetPaymentInstrument internally during the 402 auto-pay path, so the
529
+ // runtime role needs read access. CreatePaymentSession is included so
530
+ // \`agentcore invoke --auto-session\` works without a separate ManagementRole
531
+ // call. Tighten this if the SDK is updated to accept pre-fetched instrument
532
+ // details and split create-session into a backend-only flow.
533
+ env.runtime.role.addToPrincipalPolicy(
534
+ new iam.PolicyStatement({
535
+ actions: [
536
+ 'bedrock-agentcore:GetPaymentInstrument',
537
+ 'bedrock-agentcore:ListPaymentInstruments',
538
+ 'bedrock-agentcore:GetPaymentInstrumentBalance',
539
+ 'bedrock-agentcore:GetPaymentSession',
540
+ 'bedrock-agentcore:ListPaymentSessions',
541
+ 'bedrock-agentcore:CreatePaymentSession',
542
+ 'bedrock-agentcore:ProcessPayment',
543
+ ],
544
+ resources: [manager.paymentManagerArn, \`\${manager.paymentManagerArn}/*\`],
545
+ })
546
+ );
547
+
548
+ if (payment.autoPayment !== undefined) {
549
+ env.runtime.addEnvironmentVariable(\`\${prefix}_AUTO_PAYMENT\`, String(payment.autoPayment));
550
+ }
551
+ if (payment.paymentToolAllowlist) {
552
+ env.runtime.addEnvironmentVariable(\`\${prefix}_TOOL_ALLOWLIST\`, payment.paymentToolAllowlist.join(','));
553
+ }
554
+ if (payment.networkPreferences) {
555
+ env.runtime.addEnvironmentVariable(\`\${prefix}_NETWORK_PREFERENCES\`, payment.networkPreferences.join(','));
556
+ }
557
+ if (payment.authorizerType === 'CUSTOM_JWT') {
558
+ env.runtime.addEnvironmentVariable(\`\${prefix}_AUTH_MODE\`, 'bearer');
559
+ }
560
+ }
561
+
562
+ // Create connectors for this manager
563
+ for (const connector of payment.connectors) {
564
+ const connId = toCdkId(connector.name);
565
+ const conn = new AgentCorePaymentConnector(this, \`Payment\${mgrId}\${connId}\`, {
566
+ projectName: spec.name,
567
+ paymentManager: manager,
568
+ connectorName: connector.name,
569
+ connectorType: connector.provider,
570
+ credentialProviderArn: connector.credentialProviderArn,
571
+ });
572
+
573
+ // Wire first connector's ID as env var (eligible agents only)
574
+ if (connector === payment.connectors[0]) {
575
+ for (const env of this.application.environments.values()) {
576
+ if (!isPaymentEligibleAgent(env.agent)) continue;
577
+ env.runtime.addEnvironmentVariable(\`\${prefix}_CONNECTOR_ID\`, conn.paymentConnectorId);
578
+ }
579
+ }
580
+
581
+ new CfnOutput(this, \`Payment\${mgrId}\${connId}ConnectorId\`, {
582
+ value: conn.paymentConnectorId,
583
+ });
584
+ }
585
+
586
+ // CFN Outputs for post-deploy state parsing
587
+ new CfnOutput(this, \`Payment\${mgrId}ManagerArn\`, {
588
+ value: manager.paymentManagerArn,
589
+ });
590
+ new CfnOutput(this, \`Payment\${mgrId}ManagerId\`, {
591
+ value: manager.paymentManagerId,
592
+ });
593
+ new CfnOutput(this, \`Payment\${mgrId}ProcessPaymentRoleArn\`, {
594
+ value: manager.processPaymentRoleArn,
595
+ });
596
+ new CfnOutput(this, \`Payment\${mgrId}ResourceRetrievalRoleArn\`, {
597
+ value: manager.resourceRetrievalRoleArn,
598
+ });
599
+ }
600
+ }
601
+
411
602
  // Stack-level output
412
603
  new CfnOutput(this, 'StackNameOutput', {
413
604
  description: 'Name of the CloudFormation Stack',
@@ -449,7 +640,7 @@ exports[`Assets Directory Snapshots > CDK assets > cdk/cdk/package.json should m
449
640
  "@types/node": "^24.10.1",
450
641
  "jest": "^29.7.0",
451
642
  "ts-jest": "^29.2.5",
452
- "aws-cdk": "2.1100.1",
643
+ "aws-cdk": "2.1126.0",
453
644
  "prettier": "^3.4.2",
454
645
  "typescript": "~5.9.3"
455
646
  },
@@ -479,10 +670,14 @@ test('AgentCoreStack synthesizes with empty spec', () => {
479
670
  credentials: [],
480
671
  evaluators: [],
481
672
  onlineEvalConfigs: [],
673
+ configBundles: [],
482
674
  policyEngines: [],
675
+ payments: [],
483
676
  agentCoreGateways: [],
484
677
  mcpRuntimeTools: [],
485
678
  unassignedTargets: [],
679
+ datasets: [],
680
+ knowledgeBases: [],
486
681
  },
487
682
  });
488
683
  const template = Template.fromStack(stack);
@@ -543,6 +738,10 @@ exports[`Assets Directory Snapshots > File listing > should match the expected f
543
738
  "cdk/tsconfig.json",
544
739
  "container/python/Dockerfile",
545
740
  "container/python/dockerignore.template",
741
+ "container/typescript/Dockerfile",
742
+ "container/typescript/dockerignore.template",
743
+ "datasets/predefined-v1.jsonl",
744
+ "datasets/simulated-v1.jsonl",
546
745
  "evaluators/python-lambda/execution-role-policy.json",
547
746
  "evaluators/python-lambda/lambda_function.py",
548
747
  "evaluators/python-lambda/pyproject.toml",
@@ -632,14 +831,33 @@ exports[`Assets Directory Snapshots > File listing > should match the expected f
632
831
  "python/http/strands/base/mcp_client/client.py",
633
832
  "python/http/strands/base/model/__init__.py",
634
833
  "python/http/strands/base/model/load.py",
834
+ "python/http/strands/base/model/mantle_compat.py",
635
835
  "python/http/strands/base/pyproject.toml",
836
+ "python/http/strands/base/skills/fetcher.py",
837
+ "python/http/strands/capabilities/execution-limits/hooks/execution_limits.py",
636
838
  "python/http/strands/capabilities/memory/__init__.py",
637
839
  "python/http/strands/capabilities/memory/session.py",
840
+ "python/http/strands/capabilities/payments/__init__.py",
841
+ "python/http/strands/capabilities/payments/payments.py",
638
842
  "python/mcp/standalone/base/README.md",
639
843
  "python/mcp/standalone/base/gitignore.template",
640
844
  "python/mcp/standalone/base/main.py",
641
845
  "python/mcp/standalone/base/pyproject.toml",
642
846
  "typescript/.gitkeep",
847
+ "typescript/http/strands/base/README.md",
848
+ "typescript/http/strands/base/gitignore.template",
849
+ "typescript/http/strands/base/main.ts",
850
+ "typescript/http/strands/base/mcp_client/client.ts",
851
+ "typescript/http/strands/base/model/load.ts",
852
+ "typescript/http/strands/base/package.json",
853
+ "typescript/http/strands/base/tsconfig.json",
854
+ "typescript/http/strands/capabilities/memory/memory.ts",
855
+ "typescript/http/vercelai/base/README.md",
856
+ "typescript/http/vercelai/base/gitignore.template",
857
+ "typescript/http/vercelai/base/main.ts",
858
+ "typescript/http/vercelai/base/model/load.ts",
859
+ "typescript/http/vercelai/base/package.json",
860
+ "typescript/http/vercelai/base/tsconfig.json",
643
861
  ]
644
862
  `;
645
863
 
@@ -1086,7 +1304,9 @@ Thumbs.db
1086
1304
  `;
1087
1305
 
1088
1306
  exports[`Assets Directory Snapshots > Python framework assets > python/python/a2a/googleadk/base/main.py should match snapshot 1`] = `
1089
- "import os
1307
+ "{{#if needsOs}}
1308
+ import os
1309
+ {{/if}}
1090
1310
  from google.adk.agents import Agent
1091
1311
  from google.adk.a2a.executor.a2a_agent_executor import A2aAgentExecutor
1092
1312
  from google.adk.runners import Runner
@@ -1105,18 +1325,21 @@ def add_numbers(a: int, b: int) -> int:
1105
1325
 
1106
1326
  tools = [add_numbers]
1107
1327
 
1108
- {{#if sessionStorageMountPath}}
1109
- SESSION_STORAGE_PATH = "{{sessionStorageMountPath}}"
1328
+ {{#if needsOs}}
1329
+ _MOUNT_PATHS = [
1330
+ {{#if sessionStorageMountPath}}"{{sessionStorageMountPath}}",{{/if}}
1331
+ {{#each efsMounts}}"{{mountPath}}",{{/each}}
1332
+ {{#each s3Mounts}}"{{mountPath}}",{{/each}}
1333
+ ]
1110
1334
 
1111
1335
  def _safe_resolve(path: str) -> str:
1112
- """Resolve path safely within the storage boundary."""
1113
- resolved = os.path.realpath(os.path.join(SESSION_STORAGE_PATH, path.lstrip("/")))
1114
- if not resolved.startswith(os.path.realpath(SESSION_STORAGE_PATH)):
1115
- raise ValueError(f"Path '{path}' is outside the storage boundary")
1336
+ resolved = os.path.realpath(path)
1337
+ if not any(resolved == os.path.realpath(m) or resolved.startswith(os.path.realpath(m) + os.sep) for m in _MOUNT_PATHS):
1338
+ raise ValueError(f"Path '{path}' is not within any configured mount ({', '.join(_MOUNT_PATHS)})")
1116
1339
  return resolved
1117
1340
 
1118
1341
  def file_read(path: str) -> str:
1119
- """Read a file from persistent storage. The path is relative to the storage root."""
1342
+ """Read a file from a mounted filesystem. Use the absolute path (e.g. /mnt/tools/data.txt)."""
1120
1343
  try:
1121
1344
  full_path = _safe_resolve(path)
1122
1345
  with open(full_path) as f:
@@ -1127,7 +1350,7 @@ def file_read(path: str) -> str:
1127
1350
  return f"Error reading '{path}': {e.strerror}"
1128
1351
 
1129
1352
  def file_write(path: str, content: str) -> str:
1130
- """Write content to a file in persistent storage. The path is relative to the storage root."""
1353
+ """Write a file to a mounted filesystem. Use the absolute path (e.g. /mnt/tools/data.txt)."""
1131
1354
  try:
1132
1355
  full_path = _safe_resolve(path)
1133
1356
  parent = os.path.dirname(full_path)
@@ -1141,25 +1364,28 @@ def file_write(path: str, content: str) -> str:
1141
1364
  except OSError as e:
1142
1365
  return f"Error writing '{path}': {e.strerror}"
1143
1366
 
1144
- def list_files(directory: str = "") -> str:
1145
- """List files in persistent storage. The directory is relative to the storage root."""
1367
+ def list_files(path: str) -> str:
1368
+ """List files in a mounted filesystem directory. Use the absolute path (e.g. /mnt/tools)."""
1146
1369
  try:
1147
- target = _safe_resolve(directory)
1148
- entries = os.listdir(target)
1370
+ full_path = _safe_resolve(path)
1371
+ entries = os.listdir(full_path)
1149
1372
  return "\\n".join(entries) if entries else "(empty directory)"
1150
1373
  except ValueError as e:
1151
1374
  return str(e)
1152
1375
  except OSError as e:
1153
- return f"Error listing '{directory}': {e.strerror}"
1376
+ return f"Error listing '{path}': {e.strerror}"
1154
1377
 
1155
1378
  tools.extend([file_read, file_write, list_files])
1156
1379
  {{/if}}
1157
1380
 
1158
1381
  AGENT_INSTRUCTION = """
1159
1382
  You are a helpful assistant. Use tools when appropriate.
1160
- {{#if sessionStorageMountPath}}
1161
- You have persistent storage at {{sessionStorageMountPath}}. Use file tools to read and write files. Data persists across sessions.
1162
- {{/if}}
1383
+ {{#if needsOs}}
1384
+ You have access to the following mounted filesystems. Use file_read, file_write, and list_files with full absolute paths:
1385
+ {{#if sessionStorageMountPath}}- {{sessionStorageMountPath}}: ephemeral session storage (lost when session ends)
1386
+ {{/if}}{{#each efsMounts}}- {{mountPath}}: EFS persistent storage (persists across sessions and agent restarts)
1387
+ {{/each}}{{#each s3Mounts}}- {{mountPath}}: S3 Files persistent storage (durable, backed by S3)
1388
+ {{/each}}{{/if}}
1163
1389
  """
1164
1390
 
1165
1391
  agent = Agent(
@@ -1261,11 +1487,11 @@ description = "AgentCore A2A Agent using Google ADK"
1261
1487
  readme = "README.md"
1262
1488
  requires-python = ">=3.10"
1263
1489
  dependencies = [
1264
- "a2a-sdk >= 0.2.0",
1490
+ "a2a-sdk >= 0.2.0, < 1.0.0",
1265
1491
  "aws-opentelemetry-distro",
1266
1492
  "bedrock-agentcore[a2a] >= 1.0.3",
1267
- "google-adk >= 1.0.0",
1268
- "google-genai >= 1.0.0",
1493
+ "google-adk >= 1.0.0, < 2.0.0",
1494
+ "google-genai >= 1.0.0, < 2.0.0",
1269
1495
  ]
1270
1496
 
1271
1497
  [tool.hatch.build.targets.wheel]
@@ -1345,7 +1571,9 @@ Thumbs.db
1345
1571
  `;
1346
1572
 
1347
1573
  exports[`Assets Directory Snapshots > Python framework assets > python/python/a2a/langchain_langgraph/base/main.py should match snapshot 1`] = `
1348
- "import os
1574
+ "{{#if needsOs}}
1575
+ import os
1576
+ {{/if}}
1349
1577
  from langchain_core.tools import tool
1350
1578
  from langgraph.prebuilt import create_react_agent
1351
1579
  from opentelemetry.instrumentation.langchain import LangchainInstrumentor
@@ -1368,19 +1596,22 @@ def add_numbers(a: int, b: int) -> int:
1368
1596
 
1369
1597
  tools = [add_numbers]
1370
1598
 
1371
- {{#if sessionStorageMountPath}}
1372
- SESSION_STORAGE_PATH = "{{sessionStorageMountPath}}"
1599
+ {{#if needsOs}}
1600
+ _MOUNT_PATHS = [
1601
+ {{#if sessionStorageMountPath}}"{{sessionStorageMountPath}}",{{/if}}
1602
+ {{#each efsMounts}}"{{mountPath}}",{{/each}}
1603
+ {{#each s3Mounts}}"{{mountPath}}",{{/each}}
1604
+ ]
1373
1605
 
1374
1606
  def _safe_resolve(path: str) -> str:
1375
- """Resolve path safely within the storage boundary."""
1376
- resolved = os.path.realpath(os.path.join(SESSION_STORAGE_PATH, path.lstrip("/")))
1377
- if not resolved.startswith(os.path.realpath(SESSION_STORAGE_PATH)):
1378
- raise ValueError(f"Path '{path}' is outside the storage boundary")
1607
+ resolved = os.path.realpath(path)
1608
+ if not any(resolved == os.path.realpath(m) or resolved.startswith(os.path.realpath(m) + os.sep) for m in _MOUNT_PATHS):
1609
+ raise ValueError(f"Path '{path}' is not within any configured mount ({', '.join(_MOUNT_PATHS)})")
1379
1610
  return resolved
1380
1611
 
1381
1612
  @tool
1382
1613
  def file_read(path: str) -> str:
1383
- """Read a file from persistent storage. The path is relative to the storage root."""
1614
+ """Read a file from a mounted filesystem. Use the absolute path (e.g. /mnt/tools/data.txt)."""
1384
1615
  try:
1385
1616
  full_path = _safe_resolve(path)
1386
1617
  with open(full_path) as f:
@@ -1392,7 +1623,7 @@ def file_read(path: str) -> str:
1392
1623
 
1393
1624
  @tool
1394
1625
  def file_write(path: str, content: str) -> str:
1395
- """Write content to a file in persistent storage. The path is relative to the storage root."""
1626
+ """Write a file to a mounted filesystem. Use the absolute path (e.g. /mnt/tools/data.txt)."""
1396
1627
  try:
1397
1628
  full_path = _safe_resolve(path)
1398
1629
  parent = os.path.dirname(full_path)
@@ -1407,25 +1638,28 @@ def file_write(path: str, content: str) -> str:
1407
1638
  return f"Error writing '{path}': {e.strerror}"
1408
1639
 
1409
1640
  @tool
1410
- def list_files(directory: str = "") -> str:
1411
- """List files in persistent storage. The directory is relative to the storage root."""
1641
+ def list_files(path: str) -> str:
1642
+ """List files in a mounted filesystem directory. Use the absolute path (e.g. /mnt/tools)."""
1412
1643
  try:
1413
- target = _safe_resolve(directory)
1414
- entries = os.listdir(target)
1644
+ full_path = _safe_resolve(path)
1645
+ entries = os.listdir(full_path)
1415
1646
  return "\\n".join(entries) if entries else "(empty directory)"
1416
1647
  except ValueError as e:
1417
1648
  return str(e)
1418
1649
  except OSError as e:
1419
- return f"Error listing '{directory}': {e.strerror}"
1650
+ return f"Error listing '{path}': {e.strerror}"
1420
1651
 
1421
1652
  tools.extend([file_read, file_write, list_files])
1422
1653
  {{/if}}
1423
1654
 
1424
1655
  SYSTEM_PROMPT = """
1425
1656
  You are a helpful assistant. Use tools when appropriate.
1426
- {{#if sessionStorageMountPath}}
1427
- You have persistent storage at {{sessionStorageMountPath}}. Use file tools to read and write files. Data persists across sessions.
1428
- {{/if}}
1657
+ {{#if needsOs}}
1658
+ You have access to the following mounted filesystems. Use file_read, file_write, and list_files with full absolute paths:
1659
+ {{#if sessionStorageMountPath}}- {{sessionStorageMountPath}}: ephemeral session storage (lost when session ends)
1660
+ {{/if}}{{#each efsMounts}}- {{mountPath}}: EFS persistent storage (persists across sessions and agent restarts)
1661
+ {{/each}}{{#each s3Mounts}}- {{mountPath}}: S3 Files persistent storage (durable, backed by S3)
1662
+ {{/each}}{{/if}}
1429
1663
  """
1430
1664
 
1431
1665
  model = load_model()
@@ -1622,14 +1856,15 @@ description = "AgentCore A2A Agent using LangChain + LangGraph"
1622
1856
  readme = "README.md"
1623
1857
  requires-python = ">=3.10"
1624
1858
  dependencies = [
1625
- "a2a-sdk >= 0.2.0",
1859
+ "a2a-sdk >= 0.2.0, < 1.0.0",
1626
1860
  {{#if (eq modelProvider "Anthropic")}}"langchain-anthropic >= 0.3.0",
1627
1861
  {{/if}}{{#if (eq modelProvider "Bedrock")}}"langchain-aws >= 0.2.0",
1628
1862
  {{/if}}{{#if (eq modelProvider "Gemini")}}"langchain-google-genai >= 2.0.0",
1863
+ "google-genai >= 1.0.0, < 2.0.0",
1629
1864
  {{/if}}{{#if (eq modelProvider "OpenAI")}}"langchain-openai >= 0.2.0",
1630
1865
  {{/if}}"aws-opentelemetry-distro",
1631
1866
  "opentelemetry-instrumentation-langchain >= 0.59.0",
1632
- "bedrock-agentcore[a2a] >= 1.0.3",
1867
+ "bedrock-agentcore[a2a] >= 1.8.0",
1633
1868
  "botocore[crt] >= 1.35.0",
1634
1869
  "langgraph >= 0.2.0",
1635
1870
  ]
@@ -1718,7 +1953,7 @@ from model.load import load_model
1718
1953
  {{#if hasMemory}}
1719
1954
  from memory.session import get_memory_session_manager
1720
1955
  {{/if}}
1721
- {{#if sessionStorageMountPath}}
1956
+ {{#if needsOs}}
1722
1957
  import os
1723
1958
  {{/if}}
1724
1959
 
@@ -1731,19 +1966,22 @@ def add_numbers(a: int, b: int) -> int:
1731
1966
 
1732
1967
  tools = [add_numbers]
1733
1968
 
1734
- {{#if sessionStorageMountPath}}
1735
- SESSION_STORAGE_PATH = "{{sessionStorageMountPath}}"
1969
+ {{#if needsOs}}
1970
+ _MOUNT_PATHS = [
1971
+ {{#if sessionStorageMountPath}}"{{sessionStorageMountPath}}",{{/if}}
1972
+ {{#each efsMounts}}"{{mountPath}}",{{/each}}
1973
+ {{#each s3Mounts}}"{{mountPath}}",{{/each}}
1974
+ ]
1736
1975
 
1737
1976
  def _safe_resolve(path: str) -> str:
1738
- """Resolve path safely within the storage boundary."""
1739
- resolved = os.path.realpath(os.path.join(SESSION_STORAGE_PATH, path.lstrip("/")))
1740
- if not resolved.startswith(os.path.realpath(SESSION_STORAGE_PATH)):
1741
- raise ValueError(f"Path '{path}' is outside the storage boundary")
1977
+ resolved = os.path.realpath(path)
1978
+ if not any(resolved == os.path.realpath(m) or resolved.startswith(os.path.realpath(m) + os.sep) for m in _MOUNT_PATHS):
1979
+ raise ValueError(f"Path '{path}' is not within any configured mount ({', '.join(_MOUNT_PATHS)})")
1742
1980
  return resolved
1743
1981
 
1744
1982
  @tool
1745
1983
  def file_read(path: str) -> str:
1746
- """Read a file from persistent storage. The path is relative to the storage root."""
1984
+ """Read a file from a mounted filesystem. Use the absolute path (e.g. /mnt/tools/data.txt)."""
1747
1985
  try:
1748
1986
  full_path = _safe_resolve(path)
1749
1987
  with open(full_path) as f:
@@ -1755,7 +1993,7 @@ def file_read(path: str) -> str:
1755
1993
 
1756
1994
  @tool
1757
1995
  def file_write(path: str, content: str) -> str:
1758
- """Write content to a file in persistent storage. The path is relative to the storage root."""
1996
+ """Write a file to a mounted filesystem. Use the absolute path (e.g. /mnt/tools/data.txt)."""
1759
1997
  try:
1760
1998
  full_path = _safe_resolve(path)
1761
1999
  parent = os.path.dirname(full_path)
@@ -1770,25 +2008,28 @@ def file_write(path: str, content: str) -> str:
1770
2008
  return f"Error writing '{path}': {e.strerror}"
1771
2009
 
1772
2010
  @tool
1773
- def list_files(directory: str = "") -> str:
1774
- """List files in persistent storage. The directory is relative to the storage root."""
2011
+ def list_files(path: str) -> str:
2012
+ """List files in a mounted filesystem directory. Use the absolute path (e.g. /mnt/tools)."""
1775
2013
  try:
1776
- target = _safe_resolve(directory)
1777
- entries = os.listdir(target)
2014
+ full_path = _safe_resolve(path)
2015
+ entries = os.listdir(full_path)
1778
2016
  return "\\n".join(entries) if entries else "(empty directory)"
1779
2017
  except ValueError as e:
1780
2018
  return str(e)
1781
2019
  except OSError as e:
1782
- return f"Error listing '{directory}': {e.strerror}"
2020
+ return f"Error listing '{path}': {e.strerror}"
1783
2021
 
1784
2022
  tools.extend([file_read, file_write, list_files])
1785
2023
  {{/if}}
1786
2024
 
1787
2025
  SYSTEM_PROMPT = """
1788
2026
  You are a helpful assistant. Use tools when appropriate.
1789
- {{#if sessionStorageMountPath}}
1790
- You have persistent storage at {{sessionStorageMountPath}}. Use file tools to read and write files. Data persists across sessions.
1791
- {{/if}}
2027
+ {{#if needsOs}}
2028
+ You have access to the following mounted filesystems. Use file_read, file_write, and list_files with full absolute paths:
2029
+ {{#if sessionStorageMountPath}}- {{sessionStorageMountPath}}: ephemeral session storage (lost when session ends)
2030
+ {{/if}}{{#each efsMounts}}- {{mountPath}}: EFS persistent storage (persists across sessions and agent restarts)
2031
+ {{/each}}{{#each s3Mounts}}- {{mountPath}}: S3 Files persistent storage (durable, backed by S3)
2032
+ {{/each}}{{/if}}
1792
2033
  """
1793
2034
 
1794
2035
  {{#if hasMemory}}
@@ -1966,9 +2207,9 @@ readme = "README.md"
1966
2207
  requires-python = ">=3.10"
1967
2208
  dependencies = [
1968
2209
  {{#if (eq modelProvider "Anthropic")}}"anthropic >= 0.30.0",
1969
- {{/if}}"a2a-sdk[all] >= 0.2.0",
2210
+ {{/if}}"a2a-sdk[all] >= 0.2.0, < 1.0.0",
1970
2211
  "aws-opentelemetry-distro",
1971
- "bedrock-agentcore[a2a] >= 1.0.3",
2212
+ "bedrock-agentcore[a2a] >= 1.9.1",
1972
2213
  "botocore[crt] >= 1.35.0",
1973
2214
  {{#if (eq modelProvider "Gemini")}}"google-genai >= 1.0.0",
1974
2215
  {{/if}}{{#if (eq modelProvider "OpenAI")}}"openai >= 1.0.0",
@@ -1987,6 +2228,7 @@ exports[`Assets Directory Snapshots > Python framework assets > python/python/a2
1987
2228
 
1988
2229
  exports[`Assets Directory Snapshots > Python framework assets > python/python/a2a/strands/capabilities/memory/session.py should match snapshot 1`] = `
1989
2230
  "import os
2231
+ import uuid
1990
2232
  from typing import Optional
1991
2233
 
1992
2234
  from bedrock_agentcore.memory.integrations.strands.config import AgentCoreMemoryConfig{{#if memoryProviders.[0].strategies.length}}, RetrievalConfig{{/if}}
@@ -1995,10 +2237,14 @@ from bedrock_agentcore.memory.integrations.strands.session_manager import AgentC
1995
2237
  MEMORY_ID = os.getenv("{{memoryProviders.[0].envVarName}}")
1996
2238
  REGION = os.getenv("AWS_REGION")
1997
2239
 
1998
- def get_memory_session_manager(session_id: str, actor_id: str) -> Optional[AgentCoreMemorySessionManager]:
2240
+ def get_memory_session_manager(session_id: Optional[str], actor_id: str) -> Optional[AgentCoreMemorySessionManager]:
1999
2241
  if not MEMORY_ID:
2000
2242
  return None
2001
2243
 
2244
+ # AgentCoreMemoryConfig rejects None; OAuth/CUSTOM_JWT callers can reach us
2245
+ # without a runtime session header, so synthesize one when absent.
2246
+ session_id = session_id or uuid.uuid4().hex
2247
+
2002
2248
  {{#if memoryProviders.[0].strategies.length}}
2003
2249
  retrieval_config = {
2004
2250
  {{#if (includes memoryProviders.[0].strategies "SEMANTIC")}}
@@ -2007,8 +2253,11 @@ def get_memory_session_manager(session_id: str, actor_id: str) -> Optional[Agent
2007
2253
  {{#if (includes memoryProviders.[0].strategies "USER_PREFERENCE")}}
2008
2254
  f"/users/{actor_id}/preferences": RetrievalConfig(top_k=3, relevance_score=0.5),
2009
2255
  {{/if}}
2256
+ {{#if (includes memoryProviders.[0].strategies "EPISODIC")}}
2257
+ f"/episodes/{actor_id}/{session_id}": RetrievalConfig(top_k=5, relevance_score=0.5),
2258
+ {{/if}}
2010
2259
  {{#if (includes memoryProviders.[0].strategies "SUMMARIZATION")}}
2011
- f"/summaries/{actor_id}/{session_id}": RetrievalConfig(top_k=3, relevance_score=0.5),
2260
+ f"/summaries/{actor_id}": RetrievalConfig(top_k=3, relevance_score=0.5),
2012
2261
  {{/if}}
2013
2262
  }
2014
2263
  {{/if}}
@@ -2110,16 +2359,76 @@ exports[`Assets Directory Snapshots > Python framework assets > python/python/ag
2110
2359
  "import os
2111
2360
  import uvicorn
2112
2361
  from google.adk.agents import LlmAgent
2362
+ from google.adk.tools import FunctionTool
2113
2363
  from ag_ui_adk import ADKAgent, AGUIToolset, create_adk_app
2114
2364
  from model.load import load_model
2115
2365
 
2116
2366
  load_model()
2117
2367
 
2368
+ {{#if needsOs}}
2369
+ _MOUNT_PATHS = [
2370
+ {{#if sessionStorageMountPath}}"{{sessionStorageMountPath}}",{{/if}}
2371
+ {{#each efsMounts}}"{{mountPath}}",{{/each}}
2372
+ {{#each s3Mounts}}"{{mountPath}}",{{/each}}
2373
+ ]
2374
+
2375
+ def _safe_resolve(path: str) -> str:
2376
+ resolved = os.path.realpath(path)
2377
+ if not any(resolved == os.path.realpath(m) or resolved.startswith(os.path.realpath(m) + os.sep) for m in _MOUNT_PATHS):
2378
+ raise ValueError(f"Path '{path}' is not within any configured mount ({', '.join(_MOUNT_PATHS)})")
2379
+ return resolved
2380
+
2381
+ def file_read(path: str) -> str:
2382
+ """Read a file from a mounted filesystem. Use the absolute path (e.g. /mnt/tools/data.txt)."""
2383
+ try:
2384
+ full_path = _safe_resolve(path)
2385
+ with open(full_path) as f:
2386
+ return f.read()
2387
+ except ValueError as e:
2388
+ return str(e)
2389
+ except OSError as e:
2390
+ return f"Error reading '{path}': {e.strerror}"
2391
+
2392
+ def file_write(path: str, content: str) -> str:
2393
+ """Write a file to a mounted filesystem. Use the absolute path (e.g. /mnt/tools/data.txt)."""
2394
+ try:
2395
+ full_path = _safe_resolve(path)
2396
+ parent = os.path.dirname(full_path)
2397
+ if parent:
2398
+ os.makedirs(parent, exist_ok=True)
2399
+ with open(full_path, "w") as f:
2400
+ f.write(content)
2401
+ return f"Written to {path}"
2402
+ except ValueError as e:
2403
+ return str(e)
2404
+ except OSError as e:
2405
+ return f"Error writing '{path}': {e.strerror}"
2406
+
2407
+ def list_files(path: str) -> str:
2408
+ """List files in a mounted filesystem directory. Use the absolute path (e.g. /mnt/tools)."""
2409
+ try:
2410
+ full_path = _safe_resolve(path)
2411
+ entries = os.listdir(full_path)
2412
+ return "\\n".join(entries) if entries else "(empty directory)"
2413
+ except ValueError as e:
2414
+ return str(e)
2415
+ except OSError as e:
2416
+ return f"Error listing '{path}': {e.strerror}"
2417
+
2418
+ _fs_tools = [FunctionTool(file_read), FunctionTool(file_write), FunctionTool(list_files)]
2419
+ {{/if}}
2420
+
2118
2421
  agent = LlmAgent(
2119
2422
  name="{{ name }}",
2120
2423
  model="gemini-2.5-flash",
2121
- instruction="You are a helpful assistant.",
2122
- tools=[AGUIToolset()],
2424
+ instruction="""You are a helpful assistant.
2425
+ {{#if needsOs}}
2426
+ You have access to the following mounted filesystems. Use file_read, file_write, and list_files with full absolute paths:
2427
+ {{#if sessionStorageMountPath}}- {{sessionStorageMountPath}}: ephemeral session storage (lost when session ends)
2428
+ {{/if}}{{#each efsMounts}}- {{mountPath}}: EFS persistent storage (persists across sessions and agent restarts)
2429
+ {{/each}}{{#each s3Mounts}}- {{mountPath}}: S3 Files persistent storage (durable, backed by S3)
2430
+ {{/each}}{{/if}}""",
2431
+ tools=[AGUIToolset(), {{#if needsOs}}*_fs_tools{{/if}}],
2123
2432
  )
2124
2433
 
2125
2434
  adk_agent = ADKAgent(
@@ -2205,10 +2514,10 @@ requires-python = ">=3.10"
2205
2514
  dependencies = [
2206
2515
  "ag-ui-adk >= 0.6.0",
2207
2516
  "ag-ui-protocol >= 0.1.10",
2208
- "bedrock-agentcore >= 1.0.3",
2517
+ "bedrock-agentcore >= 1.8.0",
2209
2518
  "fastapi >= 0.115.12",
2210
- "google-adk >= 1.16.0",
2211
- "google-genai >= 1.0.0",
2519
+ "google-adk >= 1.16.0, < 2.0.0",
2520
+ "google-genai >= 1.0.0, < 2.0.0",
2212
2521
  "opentelemetry-distro",
2213
2522
  "opentelemetry-exporter-otlp",
2214
2523
  "uvicorn >= 0.34.3",
@@ -2318,6 +2627,62 @@ def add_numbers(a: int, b: int) -> int:
2318
2627
 
2319
2628
 
2320
2629
  backend_tools = [add_numbers]
2630
+
2631
+ {{#if needsOs}}
2632
+ _MOUNT_PATHS = [
2633
+ {{#if sessionStorageMountPath}}"{{sessionStorageMountPath}}",{{/if}}
2634
+ {{#each efsMounts}}"{{mountPath}}",{{/each}}
2635
+ {{#each s3Mounts}}"{{mountPath}}",{{/each}}
2636
+ ]
2637
+
2638
+ def _safe_resolve(path: str) -> str:
2639
+ resolved = os.path.realpath(path)
2640
+ if not any(resolved == os.path.realpath(m) or resolved.startswith(os.path.realpath(m) + os.sep) for m in _MOUNT_PATHS):
2641
+ raise ValueError(f"Path '{path}' is not within any configured mount ({', '.join(_MOUNT_PATHS)})")
2642
+ return resolved
2643
+
2644
+ @tool
2645
+ def file_read(path: str) -> str:
2646
+ """Read a file from a mounted filesystem. Use the absolute path (e.g. /mnt/tools/data.txt)."""
2647
+ try:
2648
+ full_path = _safe_resolve(path)
2649
+ with open(full_path) as f:
2650
+ return f.read()
2651
+ except ValueError as e:
2652
+ return str(e)
2653
+ except OSError as e:
2654
+ return f"Error reading '{path}': {e.strerror}"
2655
+
2656
+ @tool
2657
+ def file_write(path: str, content: str) -> str:
2658
+ """Write a file to a mounted filesystem. Use the absolute path (e.g. /mnt/tools/data.txt)."""
2659
+ try:
2660
+ full_path = _safe_resolve(path)
2661
+ parent = os.path.dirname(full_path)
2662
+ if parent:
2663
+ os.makedirs(parent, exist_ok=True)
2664
+ with open(full_path, "w") as f:
2665
+ f.write(content)
2666
+ return f"Written to {path}"
2667
+ except ValueError as e:
2668
+ return str(e)
2669
+ except OSError as e:
2670
+ return f"Error writing '{path}': {e.strerror}"
2671
+
2672
+ @tool
2673
+ def list_files(path: str) -> str:
2674
+ """List files in a mounted filesystem directory. Use the absolute path (e.g. /mnt/tools)."""
2675
+ try:
2676
+ full_path = _safe_resolve(path)
2677
+ entries = os.listdir(full_path)
2678
+ return "\\n".join(entries) if entries else "(empty directory)"
2679
+ except ValueError as e:
2680
+ return str(e)
2681
+ except OSError as e:
2682
+ return f"Error listing '{path}': {e.strerror}"
2683
+
2684
+ backend_tools.extend([file_read, file_write, list_files])
2685
+ {{/if}}
2321
2686
  model = load_model()
2322
2687
 
2323
2688
 
@@ -2517,10 +2882,11 @@ dependencies = [
2517
2882
  {{#if (eq modelProvider "Anthropic")}}"langchain-anthropic >= 0.3.0",
2518
2883
  {{/if}}{{#if (eq modelProvider "Bedrock")}}"langchain-aws >= 0.2.0",
2519
2884
  {{/if}}{{#if (eq modelProvider "Gemini")}}"langchain-google-genai >= 2.0.0",
2885
+ "google-genai >= 1.0.0, < 2.0.0",
2520
2886
  {{/if}}{{#if (eq modelProvider "OpenAI")}}"langchain-openai >= 0.2.0",
2521
2887
  {{/if}}"aws-opentelemetry-distro",
2522
2888
  "opentelemetry-instrumentation-langchain >= 0.59.0",
2523
- "bedrock-agentcore >= 1.0.3",
2889
+ "bedrock-agentcore >= 1.8.0",
2524
2890
  "botocore[crt] >= 1.35.0",
2525
2891
  "langgraph >= 0.3.25",
2526
2892
  "langchain >= 0.3.0",
@@ -2629,9 +2995,71 @@ def add_numbers(a: int, b: int) -> int:
2629
2995
 
2630
2996
  tools = [add_numbers]
2631
2997
 
2998
+ {{#if needsOs}}
2999
+ _MOUNT_PATHS = [
3000
+ {{#if sessionStorageMountPath}}"{{sessionStorageMountPath}}",{{/if}}
3001
+ {{#each efsMounts}}"{{mountPath}}",{{/each}}
3002
+ {{#each s3Mounts}}"{{mountPath}}",{{/each}}
3003
+ ]
3004
+
3005
+ def _safe_resolve(path: str) -> str:
3006
+ resolved = os.path.realpath(path)
3007
+ if not any(resolved == os.path.realpath(m) or resolved.startswith(os.path.realpath(m) + os.sep) for m in _MOUNT_PATHS):
3008
+ raise ValueError(f"Path '{path}' is not within any configured mount ({', '.join(_MOUNT_PATHS)})")
3009
+ return resolved
3010
+
3011
+ @tool
3012
+ def file_read(path: str) -> str:
3013
+ """Read a file from a mounted filesystem. Use the absolute path (e.g. /mnt/tools/data.txt)."""
3014
+ try:
3015
+ full_path = _safe_resolve(path)
3016
+ with open(full_path) as f:
3017
+ return f.read()
3018
+ except ValueError as e:
3019
+ return str(e)
3020
+ except OSError as e:
3021
+ return f"Error reading '{path}': {e.strerror}"
3022
+
3023
+ @tool
3024
+ def file_write(path: str, content: str) -> str:
3025
+ """Write a file to a mounted filesystem. Use the absolute path (e.g. /mnt/tools/data.txt)."""
3026
+ try:
3027
+ full_path = _safe_resolve(path)
3028
+ parent = os.path.dirname(full_path)
3029
+ if parent:
3030
+ os.makedirs(parent, exist_ok=True)
3031
+ with open(full_path, "w") as f:
3032
+ f.write(content)
3033
+ return f"Written to {path}"
3034
+ except ValueError as e:
3035
+ return str(e)
3036
+ except OSError as e:
3037
+ return f"Error writing '{path}': {e.strerror}"
3038
+
3039
+ @tool
3040
+ def list_files(path: str) -> str:
3041
+ """List files in a mounted filesystem directory. Use the absolute path (e.g. /mnt/tools)."""
3042
+ try:
3043
+ full_path = _safe_resolve(path)
3044
+ entries = os.listdir(full_path)
3045
+ return "\\n".join(entries) if entries else "(empty directory)"
3046
+ except ValueError as e:
3047
+ return str(e)
3048
+ except OSError as e:
3049
+ return f"Error listing '{path}': {e.strerror}"
3050
+
3051
+ tools.extend([file_read, file_write, list_files])
3052
+ {{/if}}
3053
+
2632
3054
  agent = Agent(
2633
3055
  model=load_model(),
2634
- system_prompt="You are a helpful assistant. Use tools when appropriate.",
3056
+ system_prompt="""You are a helpful assistant. Use tools when appropriate.
3057
+ {{#if needsOs}}
3058
+ You have access to the following mounted filesystems. Use file_read, file_write, and list_files with full absolute paths:
3059
+ {{#if sessionStorageMountPath}}- {{sessionStorageMountPath}}: ephemeral session storage (lost when session ends)
3060
+ {{/if}}{{#each efsMounts}}- {{mountPath}}: EFS persistent storage (persists across sessions and agent restarts)
3061
+ {{/each}}{{#each s3Mounts}}- {{mountPath}}: S3 Files persistent storage (durable, backed by S3)
3062
+ {{/each}}{{/if}}""",
2635
3063
  tools=tools,
2636
3064
  )
2637
3065
 
@@ -2800,7 +3228,7 @@ dependencies = [
2800
3228
  {{/if}}"ag-ui-strands >= 0.1.7",
2801
3229
  "ag-ui-protocol >= 0.1.10",
2802
3230
  "aws-opentelemetry-distro",
2803
- "bedrock-agentcore >= 1.0.3",
3231
+ "bedrock-agentcore >= 1.9.1",
2804
3232
  "botocore[crt] >= 1.35.0",
2805
3233
  "fastapi >= 0.115.12",
2806
3234
  {{#if (eq modelProvider "Gemini")}}"google-genai >= 1.0.0",
@@ -2822,6 +3250,7 @@ exports[`Assets Directory Snapshots > Python framework assets > python/python/ag
2822
3250
 
2823
3251
  exports[`Assets Directory Snapshots > Python framework assets > python/python/agui/strands/capabilities/memory/session.py should match snapshot 1`] = `
2824
3252
  "import os
3253
+ import uuid
2825
3254
  from typing import Optional
2826
3255
 
2827
3256
  from bedrock_agentcore.memory.integrations.strands.config import AgentCoreMemoryConfig{{#if memoryProviders.[0].strategies.length}}, RetrievalConfig{{/if}}
@@ -2830,10 +3259,14 @@ from bedrock_agentcore.memory.integrations.strands.session_manager import AgentC
2830
3259
  MEMORY_ID = os.getenv("{{memoryProviders.[0].envVarName}}")
2831
3260
  REGION = os.getenv("AWS_REGION")
2832
3261
 
2833
- def get_memory_session_manager(session_id: str, actor_id: str) -> Optional[AgentCoreMemorySessionManager]:
3262
+ def get_memory_session_manager(session_id: Optional[str], actor_id: str) -> Optional[AgentCoreMemorySessionManager]:
2834
3263
  if not MEMORY_ID:
2835
3264
  return None
2836
3265
 
3266
+ # AgentCoreMemoryConfig rejects None; OAuth/CUSTOM_JWT callers can reach us
3267
+ # without a runtime session header, so synthesize one when absent.
3268
+ session_id = session_id or uuid.uuid4().hex
3269
+
2837
3270
  {{#if memoryProviders.[0].strategies.length}}
2838
3271
  retrieval_config = {
2839
3272
  {{#if (includes memoryProviders.[0].strategies "SEMANTIC")}}
@@ -2842,8 +3275,11 @@ def get_memory_session_manager(session_id: str, actor_id: str) -> Optional[Agent
2842
3275
  {{#if (includes memoryProviders.[0].strategies "USER_PREFERENCE")}}
2843
3276
  f"/users/{actor_id}/preferences": RetrievalConfig(top_k=3, relevance_score=0.5),
2844
3277
  {{/if}}
3278
+ {{#if (includes memoryProviders.[0].strategies "EPISODIC")}}
3279
+ f"/episodes/{actor_id}/{session_id}": RetrievalConfig(top_k=5, relevance_score=0.5),
3280
+ {{/if}}
2845
3281
  {{#if (includes memoryProviders.[0].strategies "SUMMARIZATION")}}
2846
- f"/summaries/{actor_id}/{session_id}": RetrievalConfig(top_k=3, relevance_score=0.5),
3282
+ f"/summaries/{actor_id}": RetrievalConfig(top_k=3, relevance_score=0.5),
2847
3283
  {{/if}}
2848
3284
  }
2849
3285
  {{/if}}
@@ -2951,7 +3387,10 @@ Thumbs.db
2951
3387
  `;
2952
3388
 
2953
3389
  exports[`Assets Directory Snapshots > Python framework assets > python/python/http/autogen/base/main.py should match snapshot 1`] = `
2954
- "import os
3390
+ "{{#if needsOs}}
3391
+ import os
3392
+ {{/if}}
3393
+ from collections import OrderedDict
2955
3394
  from autogen_agentchat.agents import AssistantAgent
2956
3395
  from autogen_core.tools import FunctionTool
2957
3396
  from bedrock_agentcore.runtime import BedrockAgentCoreApp
@@ -2975,18 +3414,21 @@ add_numbers_tool = FunctionTool(
2975
3414
  # Define a collection of tools used by the model
2976
3415
  tools = [add_numbers_tool]
2977
3416
 
2978
- {{#if sessionStorageMountPath}}
2979
- SESSION_STORAGE_PATH = "{{sessionStorageMountPath}}"
3417
+ {{#if needsOs}}
3418
+ _MOUNT_PATHS = [
3419
+ {{#if sessionStorageMountPath}}"{{sessionStorageMountPath}}",{{/if}}
3420
+ {{#each efsMounts}}"{{mountPath}}",{{/each}}
3421
+ {{#each s3Mounts}}"{{mountPath}}",{{/each}}
3422
+ ]
2980
3423
 
2981
3424
  def _safe_resolve(path: str) -> str:
2982
- """Resolve path safely within the storage boundary."""
2983
- resolved = os.path.realpath(os.path.join(SESSION_STORAGE_PATH, path.lstrip("/")))
2984
- if not resolved.startswith(os.path.realpath(SESSION_STORAGE_PATH)):
2985
- raise ValueError(f"Path '{path}' is outside the storage boundary")
3425
+ resolved = os.path.realpath(path)
3426
+ if not any(resolved == os.path.realpath(m) or resolved.startswith(os.path.realpath(m) + os.sep) for m in _MOUNT_PATHS):
3427
+ raise ValueError(f"Path '{path}' is not within any configured mount ({', '.join(_MOUNT_PATHS)})")
2986
3428
  return resolved
2987
3429
 
2988
3430
  def file_read(path: str) -> str:
2989
- """Read a file from persistent storage. The path is relative to the storage root."""
3431
+ """Read a file from a mounted filesystem. Use the absolute path (e.g. /mnt/tools/data.txt)."""
2990
3432
  try:
2991
3433
  full_path = _safe_resolve(path)
2992
3434
  with open(full_path) as f:
@@ -2997,7 +3439,7 @@ def file_read(path: str) -> str:
2997
3439
  return f"Error reading '{path}': {e.strerror}"
2998
3440
 
2999
3441
  def file_write(path: str, content: str) -> str:
3000
- """Write content to a file in persistent storage. The path is relative to the storage root."""
3442
+ """Write a file to a mounted filesystem. Use the absolute path (e.g. /mnt/tools/data.txt)."""
3001
3443
  try:
3002
3444
  full_path = _safe_resolve(path)
3003
3445
  parent = os.path.dirname(full_path)
@@ -3011,48 +3453,73 @@ def file_write(path: str, content: str) -> str:
3011
3453
  except OSError as e:
3012
3454
  return f"Error writing '{path}': {e.strerror}"
3013
3455
 
3014
- def list_files(directory: str = "") -> str:
3015
- """List files in persistent storage. The directory is relative to the storage root."""
3456
+ def list_files(path: str) -> str:
3457
+ """List files in a mounted filesystem directory. Use the absolute path (e.g. /mnt/tools)."""
3016
3458
  try:
3017
- target = _safe_resolve(directory)
3018
- entries = os.listdir(target)
3459
+ full_path = _safe_resolve(path)
3460
+ entries = os.listdir(full_path)
3019
3461
  return "\\n".join(entries) if entries else "(empty directory)"
3020
3462
  except ValueError as e:
3021
3463
  return str(e)
3022
3464
  except OSError as e:
3023
- return f"Error listing '{directory}': {e.strerror}"
3465
+ return f"Error listing '{path}': {e.strerror}"
3024
3466
 
3025
3467
  tools.extend([
3026
- FunctionTool(file_read, description="Read a file from persistent storage. The path is relative to the storage root."),
3027
- FunctionTool(file_write, description="Write content to a file in persistent storage. The path is relative to the storage root."),
3028
- FunctionTool(list_files, description="List files in persistent storage. The directory is relative to the storage root."),
3468
+ FunctionTool(file_read, description="Read a file from a mounted filesystem. Use the absolute path (e.g. /mnt/tools/data.txt)."),
3469
+ FunctionTool(file_write, description="Write a file to a mounted filesystem. Use the absolute path (e.g. /mnt/tools/data.txt)."),
3470
+ FunctionTool(list_files, description="List files in a mounted filesystem directory. Use the absolute path (e.g. /mnt/tools)."),
3029
3471
  ])
3030
3472
  {{/if}}
3031
3473
 
3032
3474
  SYSTEM_MESSAGE = """
3033
3475
  You are a helpful assistant. Use tools when appropriate.
3034
- {{#if sessionStorageMountPath}}
3035
- You have persistent storage at {{sessionStorageMountPath}}. Use file tools to read and write files. Data persists across sessions.
3036
- {{/if}}
3476
+ {{#if needsOs}}
3477
+ You have access to the following mounted filesystems. Use file_read, file_write, and list_files with full absolute paths:
3478
+ {{#if sessionStorageMountPath}}- {{sessionStorageMountPath}}: ephemeral session storage (lost when session ends)
3479
+ {{/if}}{{#each efsMounts}}- {{mountPath}}: EFS persistent storage (persists across sessions and agent restarts)
3480
+ {{/each}}{{#each s3Mounts}}- {{mountPath}}: S3 Files persistent storage (durable, backed by S3)
3481
+ {{/each}}{{/if}}
3037
3482
  """
3038
3483
 
3039
- @app.entrypoint
3040
- async def invoke(payload, context):
3041
- log.info("Invoking Agent.....")
3484
+ # Reuses one AssistantAgent per session_id so each session keeps its own
3485
+ # in-process conversation history (best-effort; resets on cold start). Caches up
3486
+ # to 128 active sessions with LRU eviction (least-recently-used is dropped and
3487
+ # its history reset).
3488
+ _agents = OrderedDict()
3489
+
3042
3490
 
3491
+ async def get_or_create_agent(session_id):
3492
+ if session_id in _agents:
3493
+ _agents.move_to_end(session_id)
3494
+ return _agents[session_id]
3495
+ if len(_agents) >= 128:
3496
+ _agents.popitem(last=False)
3043
3497
  # Get MCP Tools
3044
3498
  mcp_tools = await get_streamable_http_mcp_tools()
3499
+ # Re-check after the await: a concurrent first-invocation for the same
3500
+ # session_id may have built and stored the agent while we were awaiting.
3501
+ # Don't overwrite it (that would orphan the agent the other request is using).
3502
+ if session_id not in _agents:
3503
+ _agents[session_id] = AssistantAgent(
3504
+ name="{{ name }}",
3505
+ model_client=load_model(),
3506
+ tools=tools + mcp_tools,
3507
+ system_message=SYSTEM_MESSAGE,
3508
+ )
3509
+ _agents.move_to_end(session_id)
3510
+ return _agents[session_id]
3045
3511
 
3046
- # Define an AssistantAgent with the model and tools
3047
- agent = AssistantAgent(
3048
- name="{{ name }}",
3049
- model_client=load_model(),
3050
- tools=tools + mcp_tools,
3051
- system_message=SYSTEM_MESSAGE,
3052
- )
3512
+
3513
+ @app.entrypoint
3514
+ async def invoke(payload, context):
3515
+ log.info("Invoking Agent.....")
3053
3516
 
3054
3517
  # Process the user prompt
3055
3518
  prompt = payload.get("prompt", "What can you help me with?")
3519
+ session_id = getattr(context, "session_id", "default-session")
3520
+
3521
+ # Reuse the per-session agent (preserves conversation history)
3522
+ agent = await get_or_create_agent(session_id)
3056
3523
 
3057
3524
  # Run the agent
3058
3525
  result = await agent.run(task=prompt)
@@ -3264,7 +3731,7 @@ dependencies = [
3264
3731
  "autogen-ext[mcp] >= 0.7.5",
3265
3732
  "opentelemetry-distro",
3266
3733
  "opentelemetry-exporter-otlp",
3267
- "bedrock-agentcore >= 1.0.3",
3734
+ "bedrock-agentcore >= 1.8.0",
3268
3735
  "botocore[crt] >= 1.35.0",
3269
3736
  "tiktoken",
3270
3737
  {{#if (eq modelProvider "Bedrock")}}
@@ -3375,7 +3842,10 @@ Thumbs.db
3375
3842
  `;
3376
3843
 
3377
3844
  exports[`Assets Directory Snapshots > Python framework assets > python/python/http/googleadk/base/main.py should match snapshot 1`] = `
3378
- "import os
3845
+ "{{#if needsOs}}
3846
+ import os
3847
+ {{/if}}
3848
+ from collections import OrderedDict
3379
3849
  from google.adk.agents import Agent
3380
3850
  from google.adk.runners import Runner
3381
3851
  from google.adk.sessions import InMemorySessionService
@@ -3406,18 +3876,21 @@ def add_numbers(a: int, b: int) -> int:
3406
3876
  # Define a collection of tools used by the model
3407
3877
  tools = [add_numbers]
3408
3878
 
3409
- {{#if sessionStorageMountPath}}
3410
- SESSION_STORAGE_PATH = "{{sessionStorageMountPath}}"
3879
+ {{#if needsOs}}
3880
+ _MOUNT_PATHS = [
3881
+ {{#if sessionStorageMountPath}}"{{sessionStorageMountPath}}",{{/if}}
3882
+ {{#each efsMounts}}"{{mountPath}}",{{/each}}
3883
+ {{#each s3Mounts}}"{{mountPath}}",{{/each}}
3884
+ ]
3411
3885
 
3412
3886
  def _safe_resolve(path: str) -> str:
3413
- """Resolve path safely within the storage boundary."""
3414
- resolved = os.path.realpath(os.path.join(SESSION_STORAGE_PATH, path.lstrip("/")))
3415
- if not resolved.startswith(os.path.realpath(SESSION_STORAGE_PATH)):
3416
- raise ValueError(f"Path '{path}' is outside the storage boundary")
3887
+ resolved = os.path.realpath(path)
3888
+ if not any(resolved == os.path.realpath(m) or resolved.startswith(os.path.realpath(m) + os.sep) for m in _MOUNT_PATHS):
3889
+ raise ValueError(f"Path '{path}' is not within any configured mount ({', '.join(_MOUNT_PATHS)})")
3417
3890
  return resolved
3418
3891
 
3419
3892
  def file_read(path: str) -> str:
3420
- """Read a file from persistent storage. The path is relative to the storage root."""
3893
+ """Read a file from a mounted filesystem. Use the absolute path (e.g. /mnt/tools/data.txt)."""
3421
3894
  try:
3422
3895
  full_path = _safe_resolve(path)
3423
3896
  with open(full_path) as f:
@@ -3428,7 +3901,7 @@ def file_read(path: str) -> str:
3428
3901
  return f"Error reading '{path}': {e.strerror}"
3429
3902
 
3430
3903
  def file_write(path: str, content: str) -> str:
3431
- """Write content to a file in persistent storage. The path is relative to the storage root."""
3904
+ """Write a file to a mounted filesystem. Use the absolute path (e.g. /mnt/tools/data.txt)."""
3432
3905
  try:
3433
3906
  full_path = _safe_resolve(path)
3434
3907
  parent = os.path.dirname(full_path)
@@ -3442,25 +3915,28 @@ def file_write(path: str, content: str) -> str:
3442
3915
  except OSError as e:
3443
3916
  return f"Error writing '{path}': {e.strerror}"
3444
3917
 
3445
- def list_files(directory: str = "") -> str:
3446
- """List files in persistent storage. The directory is relative to the storage root."""
3918
+ def list_files(path: str) -> str:
3919
+ """List files in a mounted filesystem directory. Use the absolute path (e.g. /mnt/tools)."""
3447
3920
  try:
3448
- target = _safe_resolve(directory)
3449
- entries = os.listdir(target)
3921
+ full_path = _safe_resolve(path)
3922
+ entries = os.listdir(full_path)
3450
3923
  return "\\n".join(entries) if entries else "(empty directory)"
3451
3924
  except ValueError as e:
3452
3925
  return str(e)
3453
3926
  except OSError as e:
3454
- return f"Error listing '{directory}': {e.strerror}"
3927
+ return f"Error listing '{path}': {e.strerror}"
3455
3928
 
3456
3929
  tools.extend([file_read, file_write, list_files])
3457
3930
  {{/if}}
3458
3931
 
3459
3932
  AGENT_INSTRUCTION = """
3460
3933
  I can answer your questions using the knowledge I have!
3461
- {{#if sessionStorageMountPath}}
3462
- You have persistent storage at {{sessionStorageMountPath}}. Use file tools to read and write files. Data persists across sessions.
3463
- {{/if}}
3934
+ {{#if needsOs}}
3935
+ You have access to the following mounted filesystems. Use file_read, file_write, and list_files with full absolute paths:
3936
+ {{#if sessionStorageMountPath}}- {{sessionStorageMountPath}}: ephemeral session storage (lost when session ends)
3937
+ {{/if}}{{#each efsMounts}}- {{mountPath}}: EFS persistent storage (persists across sessions and agent restarts)
3938
+ {{/each}}{{#each s3Mounts}}- {{mountPath}}: S3 Files persistent storage (durable, backed by S3)
3939
+ {{/each}}{{/if}}
3464
3940
  """
3465
3941
 
3466
3942
  # Get MCP Toolset
@@ -3490,21 +3966,57 @@ agent = Agent(
3490
3966
  )
3491
3967
 
3492
3968
 
3493
- # Session and Runner
3494
- async def setup_session_and_runner(user_id, session_id):
3495
- ensure_credentials_loaded()
3496
- session_service = InMemorySessionService()
3497
- session = await session_service.create_session(
3969
+ # Module-level session service and runner preserve history across invocations.
3970
+ # InMemorySessionService retains every (app_name, user_id, session_id) triple
3971
+ # forever, so we bound it to 128 active sessions with LRU eviction (the
3972
+ # least-recently-used session is deleted and its history reset) to keep a
3973
+ # long-running process from growing without limit. For durable history, swap in
3974
+ # a persistent session service (e.g. DatabaseSessionService).
3975
+ _SESSION_LIMIT = 128
3976
+ _session_service = InMemorySessionService()
3977
+ _session_keys = OrderedDict()
3978
+ _runner = None
3979
+
3980
+
3981
+ def get_or_create_runner():
3982
+ global _runner
3983
+ if _runner is None:
3984
+ ensure_credentials_loaded()
3985
+ _runner = Runner(
3986
+ agent=agent,
3987
+ app_name=APP_NAME,
3988
+ session_service=_session_service,
3989
+ )
3990
+ return _runner
3991
+
3992
+
3993
+ async def get_or_create_session(user_id, session_id):
3994
+ key = (user_id, session_id)
3995
+ if key in _session_keys:
3996
+ _session_keys.move_to_end(key)
3997
+ else:
3998
+ while len(_session_keys) >= _SESSION_LIMIT:
3999
+ (old_user_id, old_session_id), _ = _session_keys.popitem(last=False)
4000
+ await _session_service.delete_session(
4001
+ app_name=APP_NAME, user_id=old_user_id, session_id=old_session_id
4002
+ )
4003
+ _session_keys[key] = True
4004
+
4005
+ session = await _session_service.get_session(
3498
4006
  app_name=APP_NAME, user_id=user_id, session_id=session_id
3499
4007
  )
3500
- runner = Runner(agent=agent, app_name=APP_NAME, session_service=session_service)
3501
- return session, runner
4008
+ if session is None:
4009
+ session = await _session_service.create_session(
4010
+ app_name=APP_NAME, user_id=user_id, session_id=session_id
4011
+ )
4012
+ return session
3502
4013
 
3503
4014
 
3504
4015
  # Agent Interaction
3505
4016
  async def call_agent_async(query, user_id, session_id):
3506
4017
  content = types.Content(role="user", parts=[types.Part(text=query)])
3507
- session, runner = await setup_session_and_runner(user_id, session_id)
4018
+ runner = get_or_create_runner()
4019
+ session = await get_or_create_session(user_id, session_id)
3508
4020
  events = runner.run_async(
3509
4021
  user_id=user_id, session_id=session.id, new_message=content
3510
4022
  )
@@ -3580,20 +4092,20 @@ def get_all_gateway_mcp_toolsets() -> list[MCPToolset]:
3580
4092
  {{#each gatewayProviders}}
3581
4093
  url = os.environ.get("{{envVarName}}")
3582
4094
  if url:
3583
- {{#if (eq authType "AWS_IAM")}}
4095
+ {{#if (eq authType "AWS_IAM")}}
3584
4096
  session = create_aws_session()
3585
4097
  auth = SigV4HTTPXAuth(session.get_credentials(), "bedrock-agentcore", session.region_name)
3586
- toolsets.append(MCPToolset(connection_params=StreamableHTTPConnectionParams(
4098
+ toolsets.append(MCPToolset(tool_name_prefix="{{snakeCase name}}", connection_params=StreamableHTTPConnectionParams(
3587
4099
  url=url,
3588
4100
  httpx_client_factory=lambda **kwargs: httpx.AsyncClient(auth=auth, **kwargs)
3589
4101
  )))
3590
- {{else if (eq authType "CUSTOM_JWT")}}
4102
+ {{else if (eq authType "CUSTOM_JWT")}}
3591
4103
  token = _get_bearer_token_{{snakeCase name}}()
3592
4104
  headers = {"Authorization": f"Bearer {token}"} if token else None
3593
- toolsets.append(MCPToolset(connection_params=StreamableHTTPConnectionParams(url=url, headers=headers)))
3594
- {{else}}
3595
- toolsets.append(MCPToolset(connection_params=StreamableHTTPConnectionParams(url=url)))
3596
- {{/if}}
4105
+ toolsets.append(MCPToolset(tool_name_prefix="{{snakeCase name}}", connection_params=StreamableHTTPConnectionParams(url=url, headers=headers)))
4106
+ {{else}}
4107
+ toolsets.append(MCPToolset(tool_name_prefix="{{snakeCase name}}", connection_params=StreamableHTTPConnectionParams(url=url)))
4108
+ {{/if}}
3597
4109
  else:
3598
4110
  logger.warning("{{envVarName}} not set — {{name}} gateway tools unavailable")
3599
4111
  {{/each}}
@@ -3686,7 +4198,8 @@ requires-python = ">=3.10"
3686
4198
  dependencies = [
3687
4199
  "opentelemetry-distro",
3688
4200
  "opentelemetry-exporter-otlp",
3689
- "google-adk >= 1.17.0",
4201
+ "google-adk >= 1.35.0, < 2.0.0",
4202
+ "google-genai >= 1.0.0, < 2.0.0",
3690
4203
  "bedrock-agentcore >= 1.0.3",
3691
4204
  "botocore[crt] >= 1.35.0",
3692
4205
  {{#if hasGateway}}{{#if (includes gatewayAuthTypes "AWS_IAM")}}"mcp-proxy-for-aws >= 1.1.0",
@@ -3787,10 +4300,20 @@ Thumbs.db
3787
4300
  `;
3788
4301
 
3789
4302
  exports[`Assets Directory Snapshots > Python framework assets > python/python/http/langchain_langgraph/base/main.py should match snapshot 1`] = `
3790
- "import os
3791
- from langchain_core.messages import HumanMessage
4303
+ "{{#if needsOs}}
4304
+ import os
4305
+ {{/if}}
4306
+ from collections import OrderedDict
4307
+ from typing import Any
4308
+
4309
+ from langchain_core.messages import HumanMessage{{#if hasConfigBundle}}, SystemMessage{{/if}}
4310
+ from langgraph.checkpoint.memory import InMemorySaver
3792
4311
  from langgraph.prebuilt import create_react_agent
3793
4312
  from langchain.tools import tool
4313
+ {{#if hasConfigBundle}}
4314
+ from langchain_core.callbacks import BaseCallbackHandler
4315
+ from bedrock_agentcore.runtime.context import BedrockAgentCoreContext
4316
+ {{/if}}
3794
4317
  from opentelemetry.instrumentation.langchain import LangchainInstrumentor
3795
4318
  from bedrock_agentcore.runtime import BedrockAgentCoreApp
3796
4319
  from model.load import load_model
@@ -3814,6 +4337,17 @@ def get_or_create_model():
3814
4337
  return _llm
3815
4338
 
3816
4339
 
4340
+ DEFAULT_SYSTEM_PROMPT = """
4341
+ You are a helpful assistant. Use tools when appropriate.
4342
+ {{#if needsOs}}
4343
+ You have access to the following mounted filesystems. Use file_read, file_write, and list_files with full absolute paths:
4344
+ {{#if sessionStorageMountPath}}- {{sessionStorageMountPath}}: ephemeral session storage (lost when session ends)
4345
+ {{/if}}{{#each efsMounts}}- {{mountPath}}: EFS persistent storage (persists across sessions and agent restarts)
4346
+ {{/each}}{{#each s3Mounts}}- {{mountPath}}: S3 Files persistent storage (durable, backed by S3)
4347
+ {{/each}}{{/if}}
4348
+ """
4349
+
4350
+
3817
4351
  # Define a simple function tool
3818
4352
  @tool
3819
4353
  def add_numbers(a: int, b: int) -> int:
@@ -3824,19 +4358,42 @@ def add_numbers(a: int, b: int) -> int:
3824
4358
  # Define a collection of tools used by the model
3825
4359
  tools = [add_numbers]
3826
4360
 
3827
- {{#if sessionStorageMountPath}}
3828
- SESSION_STORAGE_PATH = "{{sessionStorageMountPath}}"
4361
+ # Module-level checkpointer preserves conversation history across invocations.
4362
+ # InMemorySaver keeps every thread_id (= session_id) checkpoint in memory
4363
+ # forever, so we bound it to 128 active threads with LRU eviction (the
4364
+ # least-recently-used thread is deleted and its history reset) to keep a
4365
+ # long-running process from growing without limit. For durable history, swap in
4366
+ # a persistent checkpointer (e.g. SqliteSaver/AsyncSqliteSaver with a file path).
4367
+ _CHECKPOINT_LIMIT = 128
4368
+ _checkpointer = InMemorySaver()
4369
+ _thread_ids = OrderedDict()
4370
+
4371
+
4372
+ def touch_thread(thread_id):
4373
+ if thread_id in _thread_ids:
4374
+ _thread_ids.move_to_end(thread_id)
4375
+ return
4376
+ while len(_thread_ids) >= _CHECKPOINT_LIMIT:
4377
+ evicted, _ = _thread_ids.popitem(last=False)
4378
+ _checkpointer.delete_thread(evicted)
4379
+ _thread_ids[thread_id] = True
4380
+
4381
+ {{#if needsOs}}
4382
+ _MOUNT_PATHS = [
4383
+ {{#if sessionStorageMountPath}}"{{sessionStorageMountPath}}",{{/if}}
4384
+ {{#each efsMounts}}"{{mountPath}}",{{/each}}
4385
+ {{#each s3Mounts}}"{{mountPath}}",{{/each}}
4386
+ ]
3829
4387
 
3830
4388
  def _safe_resolve(path: str) -> str:
3831
- """Resolve path safely within the storage boundary."""
3832
- resolved = os.path.realpath(os.path.join(SESSION_STORAGE_PATH, path.lstrip("/")))
3833
- if not resolved.startswith(os.path.realpath(SESSION_STORAGE_PATH)):
3834
- raise ValueError(f"Path '{path}' is outside the storage boundary")
4389
+ resolved = os.path.realpath(path)
4390
+ if not any(resolved == os.path.realpath(m) or resolved.startswith(os.path.realpath(m) + os.sep) for m in _MOUNT_PATHS):
4391
+ raise ValueError(f"Path '{path}' is not within any configured mount ({', '.join(_MOUNT_PATHS)})")
3835
4392
  return resolved
3836
4393
 
3837
4394
  @tool
3838
4395
  def file_read(path: str) -> str:
3839
- """Read a file from persistent storage. The path is relative to the storage root."""
4396
+ """Read a file from a mounted filesystem. Use the absolute path (e.g. /mnt/tools/data.txt)."""
3840
4397
  try:
3841
4398
  full_path = _safe_resolve(path)
3842
4399
  with open(full_path) as f:
@@ -3848,7 +4405,7 @@ def file_read(path: str) -> str:
3848
4405
 
3849
4406
  @tool
3850
4407
  def file_write(path: str, content: str) -> str:
3851
- """Write content to a file in persistent storage. The path is relative to the storage root."""
4408
+ """Write a file to a mounted filesystem. Use the absolute path (e.g. /mnt/tools/data.txt)."""
3852
4409
  try:
3853
4410
  full_path = _safe_resolve(path)
3854
4411
  parent = os.path.dirname(full_path)
@@ -3863,27 +4420,42 @@ def file_write(path: str, content: str) -> str:
3863
4420
  return f"Error writing '{path}': {e.strerror}"
3864
4421
 
3865
4422
  @tool
3866
- def list_files(directory: str = "") -> str:
3867
- """List files in persistent storage. The directory is relative to the storage root."""
4423
+ def list_files(path: str) -> str:
4424
+ """List files in a mounted filesystem directory. Use the absolute path (e.g. /mnt/tools)."""
3868
4425
  try:
3869
- target = _safe_resolve(directory)
3870
- entries = os.listdir(target)
4426
+ full_path = _safe_resolve(path)
4427
+ entries = os.listdir(full_path)
3871
4428
  return "\\n".join(entries) if entries else "(empty directory)"
3872
4429
  except ValueError as e:
3873
4430
  return str(e)
3874
4431
  except OSError as e:
3875
- return f"Error listing '{directory}': {e.strerror}"
4432
+ return f"Error listing '{path}': {e.strerror}"
3876
4433
 
3877
4434
  tools.extend([file_read, file_write, list_files])
3878
4435
  {{/if}}
3879
4436
 
3880
- SYSTEM_PROMPT = """
3881
- You are a helpful assistant. Use tools when appropriate.
3882
- {{#if sessionStorageMountPath}}
3883
- You have persistent storage at {{sessionStorageMountPath}}. Use file tools to read and write files. Data persists across sessions.
3884
- {{/if}}
3885
- """
4437
+ {{#if hasConfigBundle}}
4438
+
4439
+ class ConfigBundleCallback(BaseCallbackHandler):
4440
+ """Injects config bundle values into LangGraph agent at runtime.
4441
+
4442
+ BedrockAgentCoreContext.get_config_bundle() fetches the component configuration
4443
+ for the current runtime ARN from the config bundle service. The SDK caches the
4444
+ result and refreshes on bundle version changes.
4445
+ """
4446
+
4447
+ def on_chain_start(self, serialized: dict, inputs: dict, **kwargs: Any) -> None:
4448
+ config = BedrockAgentCoreContext.get_config_bundle()
4449
+ prompt = config.get("systemPrompt", DEFAULT_SYSTEM_PROMPT)
4450
+
4451
+ messages = inputs.get("messages", [])
4452
+ if messages and isinstance(messages[0], SystemMessage):
4453
+ messages[0] = SystemMessage(content=prompt)
4454
+ else:
4455
+ messages.insert(0, SystemMessage(content=prompt))
4456
+ inputs["messages"] = messages
3886
4457
 
4458
+ {{/if}}
3887
4459
 
3888
4460
  @app.entrypoint
3889
4461
  async def invoke(payload, context):
@@ -3901,15 +4473,47 @@ async def invoke(payload, context):
3901
4473
  if mcp_client:
3902
4474
  mcp_tools = await mcp_client.get_tools()
3903
4475
 
3904
- # Define the agent using create_react_agent
3905
- graph = create_react_agent(get_or_create_model(), tools=mcp_tools + tools, prompt=SYSTEM_PROMPT)
4476
+ # Define the agent using create_react_agent (checkpointer is shared across invocations)
4477
+ {{#if hasConfigBundle}}
4478
+ graph = create_react_agent(
4479
+ get_or_create_model(),
4480
+ tools=mcp_tools + tools,
4481
+ prompt=DEFAULT_SYSTEM_PROMPT,
4482
+ checkpointer=_checkpointer,
4483
+ )
4484
+ callback = ConfigBundleCallback()
3906
4485
 
3907
4486
  # Process the user prompt
3908
4487
  prompt = payload.get("prompt", "What can you help me with?")
4488
+ session_id = getattr(context, "session_id", "default-session")
4489
+ touch_thread(session_id)
3909
4490
  log.info(f"Agent input: {prompt}")
3910
4491
 
3911
- # Run the agent
3912
- result = await graph.ainvoke({"messages": [HumanMessage(content=prompt)]})
4492
+ # Run the agent with config bundle callback (checkpointer auto-loads/saves history per session)
4493
+ result = await graph.ainvoke(
4494
+ {"messages": [HumanMessage(content=prompt)]},
4495
+ config={"callbacks": [callback], "configurable": {"thread_id": session_id}},
4496
+ )
4497
+ {{else}}
4498
+ graph = create_react_agent(
4499
+ get_or_create_model(),
4500
+ tools=mcp_tools + tools,
4501
+ prompt=DEFAULT_SYSTEM_PROMPT,
4502
+ checkpointer=_checkpointer,
4503
+ )
4504
+
4505
+ # Process the user prompt
4506
+ prompt = payload.get("prompt", "What can you help me with?")
4507
+ session_id = getattr(context, "session_id", "default-session")
4508
+ touch_thread(session_id)
4509
+ log.info(f"Agent input: {prompt}")
4510
+
4511
+ # Run the agent (checkpointer auto-loads/saves history per session)
4512
+ result = await graph.ainvoke(
4513
+ {"messages": [HumanMessage(content=prompt)]},
4514
+ config={"configurable": {"thread_id": session_id}},
4515
+ )
4516
+ {{/if}}
3913
4517
 
3914
4518
  # Return result
3915
4519
  output = result["messages"][-1].content
@@ -3962,23 +4566,25 @@ def get_all_gateway_mcp_client() -> MultiServerMCPClient | None:
3962
4566
  {{#each gatewayProviders}}
3963
4567
  url = os.environ.get("{{envVarName}}")
3964
4568
  if url:
3965
- {{#if (eq authType "AWS_IAM")}}
4569
+ {{#if (eq authType "AWS_IAM")}}
3966
4570
  session = create_aws_session()
3967
4571
  auth = SigV4HTTPXAuth(session.get_credentials(), "bedrock-agentcore", session.region_name)
3968
- servers["{{name}}"] = {"transport": "streamable_http", "url": url, "auth": auth}
3969
- {{else if (eq authType "CUSTOM_JWT")}}
4572
+ servers["{{snakeCase name}}"] = {"transport": "streamable_http", "url": url, "auth": auth}
4573
+ {{else if (eq authType "CUSTOM_JWT")}}
3970
4574
  token = _get_bearer_token_{{snakeCase name}}()
3971
4575
  headers = {"Authorization": f"Bearer {token}"} if token else None
3972
- servers["{{name}}"] = {"transport": "streamable_http", "url": url, "headers": headers}
3973
- {{else}}
3974
- servers["{{name}}"] = {"transport": "streamable_http", "url": url}
3975
- {{/if}}
4576
+ servers["{{snakeCase name}}"] = {"transport": "streamable_http", "url": url, "headers": headers}
4577
+ {{else}}
4578
+ servers["{{snakeCase name}}"] = {"transport": "streamable_http", "url": url}
4579
+ {{/if}}
3976
4580
  else:
3977
4581
  logger.warning("{{envVarName}} not set — {{name}} gateway tools unavailable")
3978
4582
  {{/each}}
3979
4583
  if not servers:
3980
4584
  return None
3981
- return MultiServerMCPClient(servers)
4585
+ # tool_name_prefix namespaces each gateway's tools by server key so multiple
4586
+ # gateways exposing the same tool (e.g. x_amz_bedrock_agentcore_search) don't collide.
4587
+ return MultiServerMCPClient(servers, tool_name_prefix=True)
3982
4588
  {{else}}
3983
4589
  {{#if isVpc}}
3984
4590
  # VPC mode: external MCP endpoints are not reachable without a NAT gateway.
@@ -4156,9 +4762,9 @@ dependencies = [
4156
4762
  "opentelemetry-instrumentation-langchain >= 0.59.0",
4157
4763
  "langgraph >= 1.0.2",
4158
4764
  "mcp >= 1.19.0",
4159
- "langchain-mcp-adapters >= 0.1.11",
4765
+ "langchain-mcp-adapters >= 0.2.0",
4160
4766
  "langchain >= 1.0.3",
4161
- "bedrock-agentcore >= 1.0.3",
4767
+ "bedrock-agentcore >= 1.8.0",
4162
4768
  "botocore[crt] >= 1.35.0",
4163
4769
  {{#if (eq modelProvider "Bedrock")}}
4164
4770
  "langchain-aws >= 1.0.0",
@@ -4171,6 +4777,7 @@ dependencies = [
4171
4777
  {{/if}}
4172
4778
  {{#if (eq modelProvider "Gemini")}}
4173
4779
  "langchain-google-genai >= 3.0.3",
4780
+ "google-genai >= 1.0.0, < 2.0.0",
4174
4781
  {{/if}}
4175
4782
  {{#if hasGateway}}{{#if (includes gatewayAuthTypes "AWS_IAM")}}"mcp-proxy-for-aws >= 1.1.0",
4176
4783
  {{/if}}{{/if}}
@@ -4270,8 +4877,14 @@ Thumbs.db
4270
4877
  `;
4271
4878
 
4272
4879
  exports[`Assets Directory Snapshots > Python framework assets > python/python/http/openaiagents/base/main.py should match snapshot 1`] = `
4273
- "import os
4274
- from agents import Agent, Runner, function_tool
4880
+ "{{#if needsOs}}
4881
+ import os
4882
+ {{/if}}
4883
+ {{#if hasGateway}}
4884
+ from contextlib import AsyncExitStack
4885
+ {{/if}}
4886
+ from functools import lru_cache
4887
+ from agents import Agent, Runner, SQLiteSession, function_tool
4275
4888
  from bedrock_agentcore.runtime import BedrockAgentCoreApp
4276
4889
  from model.load import load_model
4277
4890
  {{#if hasGateway}}
@@ -4309,19 +4922,22 @@ def add_numbers(a: int, b: int) -> int:
4309
4922
 
4310
4923
  tools = [add_numbers]
4311
4924
 
4312
- {{#if sessionStorageMountPath}}
4313
- SESSION_STORAGE_PATH = "{{sessionStorageMountPath}}"
4925
+ {{#if needsOs}}
4926
+ _MOUNT_PATHS = [
4927
+ {{#if sessionStorageMountPath}}"{{sessionStorageMountPath}}",{{/if}}
4928
+ {{#each efsMounts}}"{{mountPath}}",{{/each}}
4929
+ {{#each s3Mounts}}"{{mountPath}}",{{/each}}
4930
+ ]
4314
4931
 
4315
4932
  def _safe_resolve(path: str) -> str:
4316
- """Resolve path safely within the storage boundary."""
4317
- resolved = os.path.realpath(os.path.join(SESSION_STORAGE_PATH, path.lstrip("/")))
4318
- if not resolved.startswith(os.path.realpath(SESSION_STORAGE_PATH)):
4319
- raise ValueError(f"Path '{path}' is outside the storage boundary")
4933
+ resolved = os.path.realpath(path)
4934
+ if not any(resolved == os.path.realpath(m) or resolved.startswith(os.path.realpath(m) + os.sep) for m in _MOUNT_PATHS):
4935
+ raise ValueError(f"Path '{path}' is not within any configured mount ({', '.join(_MOUNT_PATHS)})")
4320
4936
  return resolved
4321
4937
 
4322
4938
  @function_tool
4323
4939
  def file_read(path: str) -> str:
4324
- """Read a file from persistent storage. The path is relative to the storage root."""
4940
+ """Read a file from a mounted filesystem. Use the absolute path (e.g. /mnt/tools/data.txt)."""
4325
4941
  try:
4326
4942
  full_path = _safe_resolve(path)
4327
4943
  with open(full_path) as f:
@@ -4333,7 +4949,7 @@ def file_read(path: str) -> str:
4333
4949
 
4334
4950
  @function_tool
4335
4951
  def file_write(path: str, content: str) -> str:
4336
- """Write content to a file in persistent storage. The path is relative to the storage root."""
4952
+ """Write a file to a mounted filesystem. Use the absolute path (e.g. /mnt/tools/data.txt)."""
4337
4953
  try:
4338
4954
  full_path = _safe_resolve(path)
4339
4955
  parent = os.path.dirname(full_path)
@@ -4348,42 +4964,60 @@ def file_write(path: str, content: str) -> str:
4348
4964
  return f"Error writing '{path}': {e.strerror}"
4349
4965
 
4350
4966
  @function_tool
4351
- def list_files(directory: str = "") -> str:
4352
- """List files in persistent storage. The directory is relative to the storage root."""
4967
+ def list_files(path: str) -> str:
4968
+ """List files in a mounted filesystem directory. Use the absolute path (e.g. /mnt/tools)."""
4353
4969
  try:
4354
- target = _safe_resolve(directory)
4355
- entries = os.listdir(target)
4970
+ full_path = _safe_resolve(path)
4971
+ entries = os.listdir(full_path)
4356
4972
  return "\\n".join(entries) if entries else "(empty directory)"
4357
4973
  except ValueError as e:
4358
4974
  return str(e)
4359
4975
  except OSError as e:
4360
- return f"Error listing '{directory}': {e.strerror}"
4976
+ return f"Error listing '{path}': {e.strerror}"
4361
4977
 
4362
4978
  tools.extend([file_read, file_write, list_files])
4363
4979
  {{/if}}
4364
4980
 
4365
4981
  INSTRUCTIONS = """
4366
4982
  You are a helpful assistant. Use tools when appropriate.
4367
- {{#if sessionStorageMountPath}}
4368
- You have persistent storage at {{sessionStorageMountPath}}. Use file tools to read and write files. Data persists across sessions.
4369
- {{/if}}
4983
+ {{#if needsOs}}
4984
+ You have access to the following mounted filesystems. Use file_read, file_write, and list_files with full absolute paths:
4985
+ {{#if sessionStorageMountPath}}- {{sessionStorageMountPath}}: ephemeral session storage (lost when session ends)
4986
+ {{/if}}{{#each efsMounts}}- {{mountPath}}: EFS persistent storage (persists across sessions and agent restarts)
4987
+ {{/each}}{{#each s3Mounts}}- {{mountPath}}: S3 Files persistent storage (durable, backed by S3)
4988
+ {{/each}}{{/if}}
4370
4989
  """
4371
4990
 
4991
+ # Caches up to 128 active sessions; LRU eviction silently resets history for
4992
+ # the oldest session. For production use, replace with a durable session store
4993
+ # (e.g. SQLiteSession with a file path).
4994
+ @lru_cache(maxsize=128)
4995
+ def get_session(session_id):
4996
+ return SQLiteSession(session_id)
4997
+
4998
+
4372
4999
  # Define the agent execution
4373
- async def main(query):
5000
+ async def main(query, session):
4374
5001
  ensure_credentials_loaded()
4375
5002
  try:
4376
5003
  {{#if hasGateway}}
4377
5004
  if mcp_servers:
4378
- agent = Agent(
4379
- name="{{ name }}",
4380
- model="gpt-4.1",
4381
- instructions=INSTRUCTIONS,
4382
- mcp_servers=mcp_servers,
4383
- tools=tools
4384
- )
4385
- result = await Runner.run(agent, query)
4386
- return result
5005
+ # Connect every gateway server before the run; include_server_in_tool_names
5006
+ # namespaces each server's tools so multiple gateways exposing the same tool
5007
+ # (e.g. x_amz_bedrock_agentcore_search) don't collide.
5008
+ async with AsyncExitStack() as stack:
5009
+ for server in mcp_servers:
5010
+ await stack.enter_async_context(server)
5011
+ agent = Agent(
5012
+ name="{{ name }}",
5013
+ model="gpt-4.1",
5014
+ instructions=INSTRUCTIONS,
5015
+ mcp_servers=mcp_servers,
5016
+ tools=tools,
5017
+ mcp_config={"include_server_in_tool_names": True},
5018
+ )
5019
+ result = await Runner.run(agent, query, session=session)
5020
+ return result
4387
5021
  else:
4388
5022
  agent = Agent(
4389
5023
  name="{{ name }}",
@@ -4392,7 +5026,7 @@ async def main(query):
4392
5026
  mcp_servers=[],
4393
5027
  tools=tools
4394
5028
  )
4395
- result = await Runner.run(agent, query)
5029
+ result = await Runner.run(agent, query, session=session)
4396
5030
  return result
4397
5031
  {{else}}
4398
5032
  if mcp_servers:
@@ -4405,7 +5039,7 @@ async def main(query):
4405
5039
  mcp_servers=active_servers,
4406
5040
  tools=tools
4407
5041
  )
4408
- result = await Runner.run(agent, query)
5042
+ result = await Runner.run(agent, query, session=session)
4409
5043
  return result
4410
5044
  else:
4411
5045
  agent = Agent(
@@ -4415,7 +5049,7 @@ async def main(query):
4415
5049
  mcp_servers=[],
4416
5050
  tools=tools
4417
5051
  )
4418
- result = await Runner.run(agent, query)
5052
+ result = await Runner.run(agent, query, session=session)
4419
5053
  return result
4420
5054
  {{/if}}
4421
5055
  except Exception as e:
@@ -4429,9 +5063,11 @@ async def invoke(payload, context):
4429
5063
 
4430
5064
  # Process the user prompt
4431
5065
  prompt = payload.get("prompt", "What can you help me with?")
5066
+ session_id = getattr(context, "session_id", "default-session")
5067
+ session = get_session(session_id)
4432
5068
 
4433
- # Run the agent
4434
- result = await main(prompt)
5069
+ # Run the agent (session automatically loads/saves conversation history)
5070
+ result = await main(prompt, session)
4435
5071
 
4436
5072
  # Return result
4437
5073
  return {"result": result.final_output}
@@ -4483,20 +5119,20 @@ def get_all_gateway_mcp_servers() -> list[MCPServerStreamableHttp]:
4483
5119
  {{#each gatewayProviders}}
4484
5120
  url = os.environ.get("{{envVarName}}")
4485
5121
  if url:
4486
- {{#if (eq authType "AWS_IAM")}}
5122
+ {{#if (eq authType "AWS_IAM")}}
4487
5123
  session = create_aws_session()
4488
5124
  auth = SigV4HTTPXAuth(session.get_credentials(), "bedrock-agentcore", session.region_name)
4489
5125
  servers.append(MCPServerStreamableHttp(
4490
5126
  name="{{name}}",
4491
5127
  params={"url": url, "httpx_client_factory": lambda **kwargs: httpx.AsyncClient(auth=auth, **kwargs)}
4492
5128
  ))
4493
- {{else if (eq authType "CUSTOM_JWT")}}
5129
+ {{else if (eq authType "CUSTOM_JWT")}}
4494
5130
  token = _get_bearer_token_{{snakeCase name}}()
4495
5131
  headers = {"Authorization": f"Bearer {token}"} if token else {}
4496
5132
  servers.append(MCPServerStreamableHttp(name="{{name}}", params={"url": url, "headers": headers}))
4497
- {{else}}
5133
+ {{else}}
4498
5134
  servers.append(MCPServerStreamableHttp(name="{{name}}", params={"url": url}))
4499
- {{/if}}
5135
+ {{/if}}
4500
5136
  else:
4501
5137
  logger.warning("{{envVarName}} not set — {{name}} gateway tools unavailable")
4502
5138
  {{/each}}
@@ -4584,8 +5220,8 @@ readme = "README.md"
4584
5220
  requires-python = ">=3.10"
4585
5221
  dependencies = [
4586
5222
  "aws-opentelemetry-distro",
4587
- "openai-agents >= 0.4.2",
4588
- "bedrock-agentcore >= 1.0.3",
5223
+ "openai-agents >= 0.16.0",
5224
+ "bedrock-agentcore >= 1.8.0",
4589
5225
  "botocore[crt] >= 1.35.0",
4590
5226
  {{#if hasGateway}}{{#if (includes gatewayAuthTypes "AWS_IAM")}}"mcp-proxy-for-aws >= 1.1.0",
4591
5227
  {{/if}}{{/if}}
@@ -4684,54 +5320,293 @@ Thumbs.db"
4684
5320
  `;
4685
5321
 
4686
5322
  exports[`Assets Directory Snapshots > Python framework assets > python/python/http/strands/base/main.py should match snapshot 1`] = `
4687
- "from strands import Agent, tool
5323
+ "from typing import Any
5324
+ from collections import OrderedDict
5325
+ {{#if inlineFunctionTools}}
5326
+ import json
5327
+
5328
+ from strands.tools.tools import PythonAgentTool
5329
+ from strands.types.tools import ToolResult, ToolUse
5330
+ {{/if}}
5331
+ from strands import Agent, tool
5332
+ {{#if hasSkillsFetcher}}
5333
+ from strands import AgentSkills
5334
+ {{#if hasFetchedSkills}}
5335
+ from skills.fetcher import resolve_s3_skills, resolve_git_skills
5336
+ {{/if}}
5337
+ {{#if (some gitSkills "credentialArn")}}
5338
+ from bedrock_agentcore.services.identity import IdentityClient
5339
+ {{/if}}
5340
+ {{/if}}
5341
+ import asyncio
5342
+ {{#if hasShell}}
5343
+ import subprocess
5344
+ {{/if}}
5345
+ {{#if hasFileOperations}}
5346
+ import os
5347
+ {{/if}}
5348
+ {{#if hasExecutionLimits}}
5349
+ from strands.tools.executors import SequentialToolExecutor
5350
+ from strands.types.exceptions import EventLoopException
5351
+ from hooks.execution_limits import ExecutionLimitExceeded, ExecutionLimitsHook
5352
+ {{/if}}
5353
+ {{#if hasConfigBundle}}
5354
+ from strands.hooks import HookProvider, HookRegistry, BeforeInvocationEvent, BeforeToolCallEvent
5355
+ {{/if}}
5356
+ {{#if truncationStrategy}}
5357
+ {{#if (eq truncationStrategy "sliding_window")}}
5358
+ from strands.agent.conversation_manager.sliding_window_conversation_manager import SlidingWindowConversationManager
5359
+ {{/if}}
5360
+ {{#if (eq truncationStrategy "summarization")}}
5361
+ from strands.agent.conversation_manager.summarizing_conversation_manager import SummarizingConversationManager
5362
+ {{/if}}
5363
+ {{else}}
5364
+ from strands.agent.conversation_manager.null_conversation_manager import NullConversationManager
5365
+ {{/if}}
5366
+ {{#if hasConfigBundle}}
5367
+ from bedrock_agentcore.runtime.context import BedrockAgentCoreContext
5368
+ {{/if}}
5369
+ {{#if hasBrowser}}
5370
+ from strands_tools.browser import AgentCoreBrowser
5371
+ {{/if}}
5372
+ {{#if hasCodeInterpreter}}
5373
+ from strands_tools.code_interpreter import AgentCoreCodeInterpreter
5374
+ {{/if}}
4688
5375
  from bedrock_agentcore.runtime import BedrockAgentCoreApp
4689
5376
  from model.load import load_model
4690
5377
  {{#if hasGateway}}
4691
5378
  from mcp_client.client import get_all_gateway_mcp_clients
4692
- {{else}}
4693
- from mcp_client.client import get_streamable_http_mcp_client
4694
5379
  {{/if}}
5380
+ {{#if remoteMcpTools}}
5381
+ from mcp_client.client import get_all_remote_mcp_clients
5382
+ {{/if}}
5383
+ {{#unless (or hasGateway remoteMcpTools)}}
5384
+ {{#unless isExportHarness}}
5385
+ from mcp_client.client import get_streamable_http_mcp_client
5386
+ {{/unless}}
5387
+ {{/unless}}
4695
5388
  {{#if hasMemory}}
4696
5389
  from memory.session import get_memory_session_manager
4697
5390
  {{/if}}
4698
- {{#if sessionStorageMountPath}}
5391
+ {{#unless hasFileOperations}}
5392
+ {{#if (or needsOs browserIdentifierEnvVar codeInterpreterIdentifierEnvVar (some gitSkills "credentialArn"))}}
4699
5393
  import os
4700
5394
  {{/if}}
5395
+ {{/unless}}
5396
+ {{#if hasPayment}}
5397
+ from capabilities.payments.payments import create_payments_plugin, PAYMENT_SYSTEM_PROMPT
5398
+ {{/if}}
4701
5399
 
4702
5400
  app = BedrockAgentCoreApp()
4703
5401
  log = app.logger
4704
5402
 
4705
- # Define a Streamable HTTP MCP Client
5403
+ {{#if (or hasGateway remoteMcpTools)}}
5404
+ # Define MCP clients for all configured MCP servers (gateways and/or remote MCP)
5405
+ mcp_clients = []
4706
5406
  {{#if hasGateway}}
4707
- mcp_clients = get_all_gateway_mcp_clients()
5407
+ mcp_clients += get_all_gateway_mcp_clients()
5408
+ {{/if}}
5409
+ {{#if remoteMcpTools}}
5410
+ mcp_clients += get_all_remote_mcp_clients()
5411
+ {{/if}}
4708
5412
  {{else}}
5413
+ {{#unless isExportHarness}}
5414
+ # Define a Streamable HTTP MCP Client
4709
5415
  mcp_clients = [get_streamable_http_mcp_client()]
5416
+ {{/unless}}
5417
+ {{/if}}
5418
+
5419
+ {{#if systemPromptText}}
5420
+ DEFAULT_SYSTEM_PROMPT = """{{escapePyStr systemPromptText}}"""
5421
+ {{else}}
5422
+ DEFAULT_SYSTEM_PROMPT = """
5423
+ You are a helpful assistant. Use tools when appropriate.
5424
+ {{#if needsOs}}{{#unless isExportHarness}}
5425
+ You have access to the following mounted filesystems. Use file_read, file_write, and list_files with full absolute paths:
5426
+ {{#if sessionStorageMountPath}}- {{sessionStorageMountPath}}: ephemeral session storage (lost when session ends)
5427
+ {{/if}}{{#each efsMounts}}- {{mountPath}}: EFS persistent storage (persists across sessions and agent restarts)
5428
+ {{/each}}{{#each s3Mounts}}- {{mountPath}}: S3 Files persistent storage (durable, backed by S3)
5429
+ {{/each}}{{/unless}}{{/if}}
5430
+ """
5431
+ {{/if}}
5432
+
5433
+ {{#if hasConfigBundle}}
5434
+ DEFAULT_TOOL_DESC = "Return the sum of two numbers"
4710
5435
  {{/if}}
4711
5436
 
4712
5437
  # Define a collection of tools used by the model
4713
5438
  tools = []
4714
5439
 
5440
+ {{#if inlineFunctionTools}}
5441
+ # Inline function tools — stop the agent loop so the tool call streams back to the caller
5442
+ def _make_inline_tool(name: str, spec: dict) -> PythonAgentTool:
5443
+ def _handler(tool: ToolUse, **kwargs: Any) -> ToolResult:
5444
+ kwargs.get("request_state", {})["stop_event_loop"] = True
5445
+ return {"toolUseId": tool["toolUseId"], "status": "success", "content": [{"text": " "}]}
5446
+ _handler.__name__ = name
5447
+ return PythonAgentTool(tool_name=name, tool_spec=spec, tool_func=_handler)
5448
+
5449
+ {{#each inlineFunctionTools}}
5450
+ _INLINE_SPEC_{{snakeCase name}} = {
5451
+ "name": "{{name}}",
5452
+ "description": {{safeJson description}},
5453
+ "inputSchema": {"json": json.loads({{pyJsonStr inputSchema}}) },
5454
+ }
5455
+ tools.append(_make_inline_tool("{{name}}", _INLINE_SPEC_{{snakeCase name}}))
5456
+ {{/each}}
5457
+
5458
+ _INLINE_FUNCTION_NAMES = { {{#each inlineFunctionTools}}"{{name}}"{{#unless @last}}, {{/unless}}{{/each}} }
5459
+
5460
+ {{else}}
5461
+ _INLINE_FUNCTION_NAMES = set()
5462
+
5463
+ {{#unless isExportHarness}}
4715
5464
  # Define a simple function tool
5465
+ {{#if hasConfigBundle}}
5466
+ @tool(description=DEFAULT_TOOL_DESC)
5467
+ {{else}}
4716
5468
  @tool
5469
+ {{/if}}
4717
5470
  def add_numbers(a: int, b: int) -> int:
4718
5471
  """Return the sum of two numbers"""
4719
5472
  return a+b
4720
5473
  tools.append(add_numbers)
4721
5474
 
4722
- {{#if sessionStorageMountPath}}
4723
- SESSION_STORAGE_PATH = "{{sessionStorageMountPath}}"
5475
+ {{/unless}}
5476
+ {{/if}}
5477
+ {{#if hasBrowser}}
5478
+ {{#if browserIdentifierEnvVar}}
5479
+ _browser_id = os.getenv("{{browserIdentifierEnvVar}}")
5480
+ tools.append(AgentCoreBrowser(**({"identifier": _browser_id} if _browser_id else {})).browser)
5481
+ {{else}}
5482
+ tools.append(AgentCoreBrowser().browser)
5483
+ {{/if}}
5484
+ {{/if}}
5485
+ {{#if hasCodeInterpreter}}
5486
+ {{#if codeInterpreterIdentifierEnvVar}}
5487
+ _code_interpreter_id = os.getenv("{{codeInterpreterIdentifierEnvVar}}")
5488
+ tools.append(AgentCoreCodeInterpreter(**({"identifier": _code_interpreter_id} if _code_interpreter_id else {})).code_interpreter)
5489
+ {{else}}
5490
+ tools.append(AgentCoreCodeInterpreter().code_interpreter)
5491
+ {{/if}}
5492
+ {{/if}}
5493
+ {{#if hasShell}}
5494
+ @tool
5495
+ def shell(command: str, timeout: int = 300) -> dict:
5496
+ """Execute a bash command and return the results.
5497
+
5498
+ Args:
5499
+ command: The bash command to execute
5500
+ timeout: Timeout in seconds (default: 300)
5501
+
5502
+ Returns:
5503
+ Dict with stdout, stderr, and exit_code
5504
+ """
5505
+ result = subprocess.run(
5506
+ command, shell=True, capture_output=True, text=True, timeout=timeout
5507
+ )
5508
+ return {"stdout": result.stdout, "stderr": result.stderr, "exit_code": result.returncode}
5509
+
5510
+ tools.append(shell)
5511
+ {{/if}}
5512
+ {{#if hasFileOperations}}
5513
+ @tool
5514
+ def file_operations(
5515
+ command: str,
5516
+ path: str,
5517
+ old_str: str = None,
5518
+ new_str: str = None,
5519
+ file_text: str = None,
5520
+ insert_line: int = None,
5521
+ view_range: list = None,
5522
+ ) -> str:
5523
+ """Text editor tool for viewing and modifying files.
5524
+
5525
+ Args:
5526
+ command: The command to execute ("view", "str_replace", "create", "insert")
5527
+ path: Path to the file or directory
5528
+ old_str: Text to replace (for str_replace command)
5529
+ new_str: Replacement text (for str_replace and insert commands)
5530
+ file_text: Content for new file (for create command)
5531
+ insert_line: Line number to insert after (for insert command)
5532
+ view_range: [start_line, end_line] for viewing specific lines (for view command)
5533
+
5534
+ Returns:
5535
+ Result of the operation
5536
+ """
5537
+ try:
5538
+ if command == "view":
5539
+ if not os.path.exists(path):
5540
+ return f"Error: Path '{path}' does not exist"
5541
+ if os.path.isdir(path):
5542
+ return "\\n".join(os.listdir(path))
5543
+ with open(path) as f:
5544
+ lines = f.read().splitlines()
5545
+ if view_range:
5546
+ start, end = view_range
5547
+ start_idx = max(0, start - 1)
5548
+ end_idx = len(lines) if end == -1 else min(len(lines), end)
5549
+ lines = lines[start_idx:end_idx]
5550
+ start_num = start_idx + 1
5551
+ else:
5552
+ start_num = 1
5553
+ return "\\n".join(f"{start_num + i}: {line}" for i, line in enumerate(lines))
5554
+ elif command == "str_replace":
5555
+ if old_str is None or new_str is None:
5556
+ return "Error: str_replace requires both old_str and new_str parameters"
5557
+ if not os.path.exists(path):
5558
+ return f"Error: File '{path}' does not exist"
5559
+ content = open(path).read()
5560
+ if old_str not in content:
5561
+ return "Error: Text not found in file"
5562
+ count = content.count(old_str)
5563
+ if count > 1:
5564
+ return f"Error: Text appears {count} times in file. Please be more specific."
5565
+ open(path, "w").write(content.replace(old_str, new_str, 1))
5566
+ return f"Successfully replaced text in '{path}'"
5567
+ elif command == "create":
5568
+ if file_text is None:
5569
+ return "Error: create requires file_text parameter"
5570
+ os.makedirs(os.path.dirname(os.path.abspath(path)), exist_ok=True)
5571
+ open(path, "w").write(file_text)
5572
+ return f"Successfully created file '{path}'"
5573
+ elif command == "insert":
5574
+ if new_str is None or insert_line is None:
5575
+ return "Error: insert requires both new_str and insert_line parameters"
5576
+ if not os.path.exists(path):
5577
+ return f"Error: File '{path}' does not exist"
5578
+ lines = open(path).read().splitlines(True)
5579
+ if insert_line == 0:
5580
+ lines.insert(0, new_str + "\\n")
5581
+ elif insert_line >= len(lines):
5582
+ lines.append(new_str + "\\n")
5583
+ else:
5584
+ lines.insert(insert_line, new_str + "\\n")
5585
+ open(path, "w").write("".join(lines))
5586
+ return f"Successfully inserted text in '{path}' at line {insert_line + 1}"
5587
+ else:
5588
+ return f"Error: Unknown command '{command}'"
5589
+ except Exception as e:
5590
+ return f"Error: {e}"
5591
+
5592
+ tools.append(file_operations)
5593
+ {{/if}}
5594
+ {{#if needsOs}}{{#unless isExportHarness}}
5595
+ _MOUNT_PATHS = [
5596
+ {{#if sessionStorageMountPath}}"{{sessionStorageMountPath}}",{{/if}}
5597
+ {{#each efsMounts}}"{{mountPath}}",{{/each}}
5598
+ {{#each s3Mounts}}"{{mountPath}}",{{/each}}
5599
+ ]
4724
5600
 
4725
5601
  def _safe_resolve(path: str) -> str:
4726
- """Resolve path safely within the storage boundary."""
4727
- resolved = os.path.realpath(os.path.join(SESSION_STORAGE_PATH, path.lstrip("/")))
4728
- if not resolved.startswith(os.path.realpath(SESSION_STORAGE_PATH)):
4729
- raise ValueError(f"Path '{path}' is outside the storage boundary")
5602
+ resolved = os.path.realpath(path)
5603
+ if not any(resolved == os.path.realpath(m) or resolved.startswith(os.path.realpath(m) + os.sep) for m in _MOUNT_PATHS):
5604
+ raise ValueError(f"Path '{path}' is not within any configured mount ({', '.join(_MOUNT_PATHS)})")
4730
5605
  return resolved
4731
5606
 
4732
5607
  @tool
4733
5608
  def file_read(path: str) -> str:
4734
- """Read a file from persistent storage. The path is relative to the storage root."""
5609
+ """Read a file from a mounted filesystem. Use the absolute path (e.g. /mnt/tools/data.txt)."""
4735
5610
  try:
4736
5611
  full_path = _safe_resolve(path)
4737
5612
  with open(full_path) as f:
@@ -4743,7 +5618,7 @@ def file_read(path: str) -> str:
4743
5618
 
4744
5619
  @tool
4745
5620
  def file_write(path: str, content: str) -> str:
4746
- """Write content to a file in persistent storage. The path is relative to the storage root."""
5621
+ """Write a file to a mounted filesystem. Use the absolute path (e.g. /mnt/tools/data.txt)."""
4747
5622
  try:
4748
5623
  full_path = _safe_resolve(path)
4749
5624
  parent = os.path.dirname(full_path)
@@ -4758,82 +5633,367 @@ def file_write(path: str, content: str) -> str:
4758
5633
  return f"Error writing '{path}': {e.strerror}"
4759
5634
 
4760
5635
  @tool
4761
- def list_files(directory: str = "") -> str:
4762
- """List files in persistent storage. The directory is relative to the storage root."""
5636
+ def list_files(path: str) -> str:
5637
+ """List files in a mounted filesystem directory. Use the absolute path (e.g. /mnt/tools)."""
4763
5638
  try:
4764
- target = _safe_resolve(directory)
4765
- entries = os.listdir(target)
5639
+ full_path = _safe_resolve(path)
5640
+ entries = os.listdir(full_path)
4766
5641
  return "\\n".join(entries) if entries else "(empty directory)"
4767
5642
  except ValueError as e:
4768
5643
  return str(e)
4769
5644
  except OSError as e:
4770
- return f"Error listing '{directory}': {e.strerror}"
5645
+ return f"Error listing '{path}': {e.strerror}"
4771
5646
 
4772
5647
  tools.extend([file_read, file_write, list_files])
4773
- {{/if}}
5648
+ {{/unless}}{{/if}}
4774
5649
 
5650
+ {{#if (or hasGateway remoteMcpTools)}}
5651
+ # Add MCP clients to tools
5652
+ for mcp_client in mcp_clients:
5653
+ if mcp_client:
5654
+ tools.append(mcp_client)
5655
+ {{else}}
5656
+ {{#unless isExportHarness}}
4775
5657
  # Add MCP client to tools if available
4776
5658
  for mcp_client in mcp_clients:
4777
5659
  if mcp_client:
4778
5660
  tools.append(mcp_client)
5661
+ {{/unless}}
5662
+ {{/if}}
5663
+
5664
+ {{#if hasConfigBundle}}
5665
+
5666
+ class ConfigBundleHook(HookProvider):
5667
+ """Injects config bundle values (system prompt, tool descriptions) before each invocation.
5668
+
5669
+ BedrockAgentCoreContext.get_config_bundle() fetches the component configuration
5670
+ for the current runtime ARN from the config bundle service. The SDK caches the
5671
+ result and refreshes on bundle version changes.
5672
+ """
5673
+
5674
+ def register_hooks(self, registry: HookRegistry, **kwargs: Any) -> None:
5675
+ registry.add_callback(BeforeInvocationEvent, self._inject_system_prompt)
5676
+ registry.add_callback(BeforeToolCallEvent, self._override_tool_desc)
5677
+
5678
+ def _inject_system_prompt(self, event: BeforeInvocationEvent) -> None:
5679
+ config = BedrockAgentCoreContext.get_config_bundle()
5680
+ prompt = config.get("systemPrompt", DEFAULT_SYSTEM_PROMPT)
5681
+
5682
+ if prompt != event.agent.system_prompt:
5683
+ event.agent.system_prompt = prompt
5684
+
5685
+ def _override_tool_desc(self, event: BeforeToolCallEvent) -> None:
5686
+ config = BedrockAgentCoreContext.get_config_bundle()
5687
+ tool_descs = config.get("toolDescriptions", {})
5688
+
5689
+ tool_name = event.tool_use["name"]
5690
+ override = tool_descs.get(tool_name)
5691
+ if override and event.selected_tool:
5692
+ spec = event.selected_tool.tool_spec
5693
+ if spec and "description" in spec:
5694
+ spec["description"] = override
4779
5695
 
4780
- SYSTEM_PROMPT = """
4781
- You are a helpful assistant. Use tools when appropriate.
4782
- {{#if sessionStorageMountPath}}
4783
- You have persistent storage at {{sessionStorageMountPath}}. Use file tools to read and write files. Data persists across sessions.
4784
5696
  {{/if}}
4785
- """
5697
+
5698
+ def _make_conversation_manager():
5699
+ {{#if truncationStrategy}}
5700
+ {{#if (eq truncationStrategy "sliding_window")}}
5701
+ {{#if truncationConfig}}
5702
+ return SlidingWindowConversationManager(**{{safeJson truncationConfig}}, per_turn=True)
5703
+ {{else}}
5704
+ return SlidingWindowConversationManager(per_turn=True)
5705
+ {{/if}}
5706
+ {{else}}
5707
+ {{#if truncationConfig}}
5708
+ return SummarizingConversationManager(**{{safeJson truncationConfig}})
5709
+ {{else}}
5710
+ return SummarizingConversationManager()
5711
+ {{/if}}
5712
+ {{/if}}
5713
+ {{else}}
5714
+ return NullConversationManager()
5715
+ {{/if}}
4786
5716
 
4787
5717
  {{#if hasMemory}}
5718
+ {{#unless hasPayment}}
4788
5719
  def agent_factory():
4789
5720
  cache = {}
4790
- def get_or_create_agent(session_id, user_id):
4791
- key = f"{session_id}/{user_id}"
5721
+ def get_or_create_agent(session_id, user_id{{#if hasSkillsFetcher}}, skill_plugins=None{{/if}}):
5722
+ {{#if actorId}}
5723
+ _actor_id = "{{actorId}}"
5724
+ {{else}}
5725
+ _actor_id = user_id
5726
+ {{/if}}
5727
+ key = f"{session_id}/{_actor_id}"
4792
5728
  if key not in cache:
4793
- # Create an agent for the given session_id and user_id
4794
5729
  cache[key] = Agent(
4795
5730
  model=load_model(),
4796
- session_manager=get_memory_session_manager(session_id, user_id),
4797
- system_prompt=SYSTEM_PROMPT,
4798
- tools=tools
5731
+ session_manager=get_memory_session_manager(session_id, _actor_id),
5732
+ conversation_manager=_make_conversation_manager(),
5733
+ system_prompt=DEFAULT_SYSTEM_PROMPT,
5734
+ tools=tools,
5735
+ {{#if hasSkillsFetcher}}
5736
+ plugins=skill_plugins or None,
5737
+ {{/if}}
5738
+ {{#if hasExecutionLimits}}
5739
+ tool_executor=SequentialToolExecutor(),
5740
+ callback_handler=None,
5741
+ {{/if}}
5742
+ hooks=[
5743
+ {{#if hasExecutionLimits}}
5744
+ ExecutionLimitsHook(
5745
+ {{#if maxIterations}}max_iterations={{maxIterations}},{{/if}}
5746
+ {{#if maxTokens}}max_tokens={{maxTokens}},{{/if}}
5747
+ {{#if timeoutSeconds}}timeout_seconds={{timeoutSeconds}},{{/if}}
5748
+ ),
5749
+ {{/if}}
5750
+ {{#if hasConfigBundle}}
5751
+ ConfigBundleHook(),
5752
+ {{/if}}
5753
+ ],
4799
5754
  )
4800
5755
  return cache[key]
4801
5756
  return get_or_create_agent
4802
5757
  get_or_create_agent = agent_factory()
5758
+ {{/unless}}
4803
5759
  {{else}}
4804
- _agent = None
4805
-
4806
- def get_or_create_agent():
4807
- global _agent
4808
- if _agent is None:
4809
- _agent = Agent(
5760
+ {{#unless hasPayment}}
5761
+ # Reuses one Agent per session_id so each session keeps its own in-process
5762
+ # conversation history (best-effort; resets on cold start). The cache is bounded
5763
+ # to 128 sessions with LRU eviction (least-recently-used is dropped and its
5764
+ # history reset) so a single process serving many sessions cannot leak history
5765
+ # between them or grow without limit. For durable history, attach a session manager.
5766
+ def agent_factory():
5767
+ cache = OrderedDict()
5768
+ def get_or_create_agent(session_id{{#if hasSkillsFetcher}}, skill_plugins=None{{/if}}):
5769
+ if session_id in cache:
5770
+ cache.move_to_end(session_id)
5771
+ return cache[session_id]
5772
+ if len(cache) >= 128:
5773
+ cache.popitem(last=False)
5774
+ cache[session_id] = Agent(
4810
5775
  model=load_model(),
4811
- system_prompt=SYSTEM_PROMPT,
4812
- tools=tools
5776
+ system_prompt=DEFAULT_SYSTEM_PROMPT,
5777
+ tools=tools,
5778
+ conversation_manager=_make_conversation_manager(),
5779
+ {{#if hasSkillsFetcher}}
5780
+ plugins=skill_plugins or None,
5781
+ {{/if}}
5782
+ {{#if hasExecutionLimits}}
5783
+ tool_executor=SequentialToolExecutor(),
5784
+ callback_handler=None,
5785
+ {{/if}}
5786
+ hooks=[
5787
+ {{#if hasExecutionLimits}}
5788
+ ExecutionLimitsHook(
5789
+ {{#if maxIterations}}max_iterations={{maxIterations}},{{/if}}
5790
+ {{#if maxTokens}}max_tokens={{maxTokens}},{{/if}}
5791
+ {{#if timeoutSeconds}}timeout_seconds={{timeoutSeconds}},{{/if}}
5792
+ ),
5793
+ {{/if}}
5794
+ {{#if hasConfigBundle}}
5795
+ ConfigBundleHook(),
5796
+ {{/if}}
5797
+ ],
4813
5798
  )
4814
- return _agent
5799
+ return cache[session_id]
5800
+ return get_or_create_agent
5801
+ get_or_create_agent = agent_factory()
5802
+ {{/unless}}
4815
5803
  {{/if}}
4816
5804
 
4817
5805
 
5806
+ def _extract_prompt(payload: dict):
5807
+ """Accept harness-style messages[], tool_results[], or plain prompt string payloads."""
5808
+ if "messages" in payload:
5809
+ return payload["messages"]
5810
+ if "tool_results" in payload:
5811
+ return [{"role": "user", "content": [{"toolResult": {
5812
+ "toolUseId": tr["toolUseId"],
5813
+ "status": tr.get("status", "success"),
5814
+ "content": tr.get("content", []),
5815
+ }} for tr in payload["tool_results"]]}]
5816
+ return payload.get("prompt", "")
5817
+
5818
+
5819
+ def _has_inline_function_call(messages) -> bool:
5820
+ """Return True if messages contains an assistant toolUse for an inline function tool."""
5821
+ if not _INLINE_FUNCTION_NAMES or not isinstance(messages, list):
5822
+ return False
5823
+ for msg in messages:
5824
+ if msg.get("role") == "assistant":
5825
+ for block in msg.get("content", []):
5826
+ if isinstance(block, dict) and block.get("toolUse", {}).get("name") in _INLINE_FUNCTION_NAMES:
5827
+ return True
5828
+ return False
5829
+
5830
+
5831
+ def _is_inline_function_call(event: dict) -> bool:
5832
+ """Check if a contentBlockStart event is for an inline function tool."""
5833
+ if not _INLINE_FUNCTION_NAMES:
5834
+ return False
5835
+ cbs = event.get("contentBlockStart", {})
5836
+ start = cbs.get("start", {})
5837
+ tool_use = start.get("toolUse") if isinstance(start, dict) else None
5838
+ return tool_use is not None and tool_use.get("name") in _INLINE_FUNCTION_NAMES
5839
+
5840
+
5841
+
4818
5842
  @app.entrypoint
4819
5843
  async def invoke(payload, context):
4820
5844
  log.info("Invoking Agent.....")
4821
5845
 
5846
+ {{#if hasPayment}}
5847
+ user_id = payload.get("user_id") or getattr(context, "user_id", "default-user")
5848
+ instrument_id = payload.get("payment_instrument_id")
5849
+ session_id = payload.get("payment_session_id")
5850
+ payments_plugin = create_payments_plugin(user_id, instrument_id, session_id)
5851
+ plugins = [payments_plugin] if payments_plugin else []
5852
+ {{/if}}
5853
+ {{#if hasSkillsFetcher}}
5854
+ skill_paths = [{{#each pathSkills}}{{safeJson this}}{{#unless @last}}, {{/unless}}{{/each}}]
5855
+ {{#if s3Skills}}
5856
+ s3_skill_sources = [{{#each s3Skills}}{{safeJson this}}{{#unless @last}}, {{/unless}}{{/each}}]
5857
+ skill_paths.extend(await asyncio.to_thread(resolve_s3_skills, s3_skill_sources, None))
5858
+ {{/if}}
5859
+ {{#if gitSkills}}
5860
+ git_skill_sources = [
5861
+ {{#each gitSkills}}
5862
+ 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}}),
5863
+ {{/each}}
5864
+ ]
5865
+ {{#if (some gitSkills "credentialArn")}}
5866
+ _git_identity_client = IdentityClient(os.environ.get("AWS_REGION", os.environ.get("AWS_DEFAULT_REGION", "us-east-1")))
5867
+ {{else}}
5868
+ _git_identity_client = None
5869
+ {{/if}}
5870
+ skill_paths.extend(await asyncio.to_thread(resolve_git_skills, git_skill_sources, _git_identity_client))
5871
+ {{/if}}
5872
+ _skill_plugins = [AgentSkills(skills=skill_paths)] if skill_paths else []
5873
+ {{/if}}
5874
+
4822
5875
  {{#if hasMemory}}
5876
+ {{#if hasPayment}}
5877
+ mem_session_id = getattr(context, 'session_id', 'default-session')
5878
+ {{#if actorId}}
5879
+ mem_user_id = "{{actorId}}"
5880
+ {{else}}
5881
+ mem_user_id = getattr(context, 'user_id', 'default-user')
5882
+ {{/if}}
5883
+ agent = Agent(
5884
+ model=load_model(),
5885
+ session_manager=get_memory_session_manager(mem_session_id, mem_user_id),
5886
+ system_prompt=DEFAULT_SYSTEM_PROMPT + PAYMENT_SYSTEM_PROMPT,
5887
+ tools=tools,
5888
+ plugins=plugins{{#if hasSkillsFetcher}} + _skill_plugins{{/if}},{{#if hasConfigBundle}}
5889
+ hooks=[ConfigBundleHook()],{{/if}}
5890
+ )
5891
+ {{else}}
4823
5892
  session_id = getattr(context, 'session_id', 'default-session')
5893
+ {{#if actorId}}
5894
+ user_id = "{{actorId}}"
5895
+ {{else}}
4824
5896
  user_id = getattr(context, 'user_id', 'default-user')
4825
- agent = get_or_create_agent(session_id, user_id)
5897
+ {{/if}}
5898
+ agent = get_or_create_agent(session_id, user_id{{#if hasSkillsFetcher}}, _skill_plugins{{/if}})
5899
+ {{/if}}
5900
+ {{else}}
5901
+ {{#if hasPayment}}
5902
+ agent = Agent(
5903
+ model=load_model(),
5904
+ system_prompt=DEFAULT_SYSTEM_PROMPT + PAYMENT_SYSTEM_PROMPT,
5905
+ tools=tools,
5906
+ plugins=plugins{{#if hasSkillsFetcher}} + _skill_plugins{{/if}},{{#if hasConfigBundle}}
5907
+ hooks=[ConfigBundleHook()],{{/if}}
5908
+ )
4826
5909
  {{else}}
4827
- agent = get_or_create_agent()
5910
+ session_id = getattr(context, 'session_id', 'default-session')
5911
+ agent = get_or_create_agent(session_id{{#if hasSkillsFetcher}}, _skill_plugins{{/if}})
4828
5912
  {{/if}}
5913
+ {{/if}}
5914
+
5915
+ prompt = _extract_prompt(payload)
5916
+
5917
+ {{#if inlineFunctionTools}}
5918
+ # If Turn 2 carries the harness-style assistant(toolUse)+user(toolResult) pair,
5919
+ # strip the placeholder turn Strands stored during Turn 1 so the real toolResult
5920
+ # is injected cleanly — same protocol as the harness runtime.
5921
+ if _has_inline_function_call(prompt):
5922
+ msgs = agent.messages
5923
+ if len(msgs) >= 2 and any("toolResult" in b for b in msgs[-1].get("content", [])):
5924
+ del msgs[-2:]
5925
+ {{/if}}
4829
5926
 
4830
- # Execute and format response
4831
- stream = agent.stream_async(payload.get("prompt"))
5927
+ {{#if hasExecutionLimits}}
5928
+ timeout_seconds = {{#if timeoutSeconds}}{{timeoutSeconds}}{{else}}None{{/if}}
5929
+ timeout_fired = False
5930
+ watchdog_task = None
5931
+ if timeout_seconds is not None:
5932
+ async def _timeout_watchdog():
5933
+ nonlocal timeout_fired
5934
+ await asyncio.sleep(timeout_seconds)
5935
+ timeout_fired = True
5936
+ agent.cancel()
5937
+ watchdog_task = asyncio.create_task(_timeout_watchdog())
4832
5938
 
4833
- async for event in stream:
4834
- # Handle Text parts of the response
4835
- if "data" in event and isinstance(event["data"], str):
4836
- yield event["data"]
5939
+ try:
5940
+ {{#if inlineFunctionTools}}
5941
+ hit_inline_function = False
5942
+ {{/if}}
5943
+ async for event in agent.stream_async(
5944
+ prompt,
5945
+ ):
5946
+ if not isinstance(event, dict) or "event" not in event:
5947
+ continue
5948
+ cbs = event["event"].get("contentBlockStart")
5949
+ if cbs is not None and not cbs.get("start"):
5950
+ continue
5951
+ {{#if inlineFunctionTools}}
5952
+ if not hit_inline_function:
5953
+ hit_inline_function = _is_inline_function_call(event["event"])
5954
+ {{/if}}
5955
+ yield event
5956
+ {{#if inlineFunctionTools}}
5957
+ if hit_inline_function and "messageStop" in event["event"]:
5958
+ return
5959
+ {{/if}}
5960
+
5961
+ if timeout_fired:
5962
+ yield {"event": {"messageStop": {"stopReason": "timeout_exceeded"}}}
5963
+ except EventLoopException as e:
5964
+ if isinstance(e.original_exception, ExecutionLimitExceeded):
5965
+ yield {"event": {"messageStop": {"stopReason": str(e.original_exception)}}}
5966
+ return
5967
+ raise
5968
+ finally:
5969
+ if watchdog_task is not None:
5970
+ watchdog_task.cancel()
5971
+ try:
5972
+ await watchdog_task
5973
+ except asyncio.CancelledError:
5974
+ pass
5975
+ {{else}}
5976
+ {{#if inlineFunctionTools}}
5977
+ hit_inline_function = False
5978
+ {{/if}}
5979
+ async for event in agent.stream_async(
5980
+ prompt,
5981
+ ):
5982
+ if not isinstance(event, dict) or "event" not in event:
5983
+ continue
5984
+ cbs = event["event"].get("contentBlockStart")
5985
+ if cbs is not None and not cbs.get("start"):
5986
+ continue
5987
+ {{#if inlineFunctionTools}}
5988
+ if not hit_inline_function:
5989
+ hit_inline_function = _is_inline_function_call(event["event"])
5990
+ {{/if}}
5991
+ yield event
5992
+ {{#if inlineFunctionTools}}
5993
+ if hit_inline_function and "messageStop" in event["event"]:
5994
+ return
5995
+ {{/if}}
5996
+ {{/if}}
4837
5997
 
4838
5998
 
4839
5999
  if __name__ == "__main__":
@@ -4867,7 +6027,10 @@ from bedrock_agentcore.identity import requires_access_token
4867
6027
  @requires_access_token(
4868
6028
  provider_name="{{credentialProviderName}}",
4869
6029
  scopes=[{{#if scopes}}"{{scopes}}"{{/if}}],
4870
- auth_flow="M2M",
6030
+ auth_flow="{{#if authFlow}}{{authFlow}}{{else}}M2M{{/if}}",
6031
+ {{#if customParameters}}
6032
+ custom_parameters={{safeJson customParameters}},
6033
+ {{/if}}
4871
6034
  )
4872
6035
  def _get_bearer_token_{{snakeCase name}}(*, access_token: str):
4873
6036
  """Obtain OAuth access token via AgentCore Identity for {{name}}."""
@@ -4878,18 +6041,22 @@ def _get_bearer_token_{{snakeCase name}}(*, access_token: str):
4878
6041
  {{#each gatewayProviders}}
4879
6042
  def get_{{snakeCase name}}_mcp_client() -> MCPClient | None:
4880
6043
  """Returns an MCP Client connected to the {{name}} gateway."""
6044
+ {{#if hardcodedUrl}}
6045
+ url = {{safeJson hardcodedUrl}}
6046
+ {{else}}
4881
6047
  url = os.environ.get("{{envVarName}}")
4882
6048
  if not url:
4883
6049
  logger.warning("{{envVarName}} not set — {{name}} gateway tools unavailable")
4884
6050
  return None
6051
+ {{/if}}
4885
6052
  {{#if (eq authType "AWS_IAM")}}
4886
- 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"))))
6053
+ 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}}")
4887
6054
  {{else if (eq authType "CUSTOM_JWT")}}
4888
6055
  token = _get_bearer_token_{{snakeCase name}}()
4889
6056
  headers = {"Authorization": f"Bearer {token}"} if token else {}
4890
- return MCPClient(lambda: streamablehttp_client(url, headers=headers))
6057
+ return MCPClient(lambda: streamablehttp_client(url, headers=headers), prefix="{{snakeCase name}}")
4891
6058
  {{else}}
4892
- return MCPClient(lambda: streamablehttp_client(url))
6059
+ return MCPClient(lambda: streamablehttp_client(url), prefix="{{snakeCase name}}")
4893
6060
  {{/if}}
4894
6061
 
4895
6062
  {{/each}}
@@ -4902,7 +6069,41 @@ def get_all_gateway_mcp_clients() -> list[MCPClient]:
4902
6069
  clients.append(client)
4903
6070
  {{/each}}
4904
6071
  return clients
4905
- {{else}}
6072
+ {{/if}}
6073
+ {{#if remoteMcpTools}}
6074
+ {{#if (some remoteMcpTools "headerCredentials")}}
6075
+ from bedrock_agentcore.identity.auth import requires_api_key
6076
+ {{/if}}
6077
+ {{#each remoteMcpTools}}
6078
+ {{#if headerCredentials}}
6079
+ {{#each headerCredentials}}
6080
+ @requires_api_key(provider_name="{{credentialName}}")
6081
+ def _get_{{snakeCase ../name}}_{{snakeCase headerKey}}_key(api_key: str) -> str:
6082
+ """Fetch {{headerKey}} credential for {{../name}} from AgentCore Identity."""
6083
+ return api_key
6084
+
6085
+ {{/each}}
6086
+ {{/if}}
6087
+ def get_{{snakeCase name}}_mcp_client() -> MCPClient | None:
6088
+ """Returns an MCP Client for the {{name}} remote MCP server."""
6089
+ url = {{safeJson url}}
6090
+ {{#if headerCredentials}}
6091
+ if os.getenv("LOCAL_DEV") == "1":
6092
+ headers = { {{#each headerCredentials}}{{safeJson headerKey}}: os.environ.get("{{envVarName}}", ""){{#unless @last}}, {{/unless}}{{/each}} }
6093
+ else:
6094
+ headers = { {{#each headerCredentials}}{{safeJson headerKey}}: _get_{{snakeCase ../name}}_{{snakeCase headerKey}}_key(){{#unless @last}}, {{/unless}}{{/each}} }
6095
+ return MCPClient(lambda: streamablehttp_client(url, headers=headers))
6096
+ {{else}}
6097
+ return MCPClient(lambda: streamablehttp_client(url))
6098
+ {{/if}}
6099
+
6100
+ {{/each}}
6101
+ def get_all_remote_mcp_clients() -> list[MCPClient]:
6102
+ """Returns all configured remote MCP clients."""
6103
+ clients = [{{#each remoteMcpTools}}get_{{snakeCase name}}_mcp_client(){{#unless @last}}, {{/unless}}{{/each}}]
6104
+ return [c for c in clients if c is not None]
6105
+ {{/if}}
6106
+ {{#unless (or hasGateway remoteMcpTools)}}
4906
6107
  {{#if isVpc}}
4907
6108
  # VPC mode: external MCP endpoints are not reachable without a NAT gateway.
4908
6109
  # Add an AgentCore Gateway with \`agentcore add gateway\`, or configure your own endpoint below.
@@ -4911,6 +6112,7 @@ def get_streamable_http_mcp_client() -> MCPClient | None:
4911
6112
  """No MCP server configured. Add a gateway with \`agentcore add gateway\`."""
4912
6113
  return None
4913
6114
  {{else}}
6115
+ {{#unless isExportHarness}}
4914
6116
  # ExaAI provides information about code through web searches, crawling and code context searches through their platform. Requires no authentication
4915
6117
  EXAMPLE_MCP_ENDPOINT = "https://mcp.exa.ai/mcp"
4916
6118
 
@@ -4918,8 +6120,9 @@ def get_streamable_http_mcp_client() -> MCPClient:
4918
6120
  """Returns an MCP Client compatible with Strands"""
4919
6121
  # to use an MCP server that supports bearer authentication, add headers={"Authorization": f"Bearer {access_token}"}
4920
6122
  return MCPClient(lambda: streamablehttp_client(EXAMPLE_MCP_ENDPOINT))
6123
+ {{/unless}}
4921
6124
  {{/if}}
4922
- {{/if}}
6125
+ {{/unless}}
4923
6126
  "
4924
6127
  `;
4925
6128
 
@@ -4930,12 +6133,74 @@ exports[`Assets Directory Snapshots > Python framework assets > python/python/ht
4930
6133
 
4931
6134
  exports[`Assets Directory Snapshots > Python framework assets > python/python/http/strands/base/model/load.py should match snapshot 1`] = `
4932
6135
  "{{#if (eq modelProvider "Bedrock")}}
6136
+ {{#if bedrockMantle}}
6137
+ import os
6138
+
6139
+ from aws_bedrock_token_generator import provide_token
6140
+ {{#if (eq mantleApiFormat "chat_completions")}}
6141
+ from strands.models.openai import OpenAIModel
6142
+ {{else}}
6143
+ {{#if mantleProprietary}}
6144
+ from strands.models.openai_responses import OpenAIResponsesModel
6145
+ {{else}}
6146
+ from model.mantle_compat import MantleCompatResponsesModel
6147
+ {{/if}}
6148
+ {{/if}}
6149
+
6150
+ MODEL_ID = "{{modelId}}"
6151
+
6152
+
6153
+ def load_model():
6154
+ """
6155
+ Get a Bedrock Mantle model client. These OpenAI-compatible models (e.g. openai.gpt-5.5,
6156
+ openai.gpt-oss-120b) are served via the Bedrock Mantle endpoint, NOT the Converse API — so they
6157
+ are invoked through an OpenAI-style client authenticated with a short-lived Bedrock bearer token.
6158
+ Region is read from AWS_REGION (set by the AgentCore runtime).
6159
+ """
6160
+ region = os.environ.get("AWS_REGION", os.environ.get("AWS_DEFAULT_REGION", "us-east-1"))
6161
+ token = provide_token(region=region)
6162
+ {{#if mantleProprietary}}
6163
+ # Proprietary OpenAI models only work on the /openai/v1 Mantle path.
6164
+ base_url = f"https://bedrock-mantle.{region}.api.aws/openai/v1"
6165
+ {{else}}
6166
+ # Open-source OpenAI models (gpt-oss-*) only work on the /v1 Mantle path.
6167
+ base_url = f"https://bedrock-mantle.{region}.api.aws/v1"
6168
+ {{/if}}
6169
+ client_args = {"api_key": token, "base_url": base_url}
6170
+
6171
+ params = {}
6172
+ {{#if modelMaxTokens}}
6173
+ {{#if (eq mantleApiFormat "chat_completions")}}
6174
+ params["max_completion_tokens"] = {{modelMaxTokens}}
6175
+ {{else}}
6176
+ params["max_output_tokens"] = {{modelMaxTokens}}
6177
+ {{/if}}
6178
+ {{/if}}
6179
+ {{#if modelTemperature}}
6180
+ params["temperature"] = {{modelTemperature}}
6181
+ {{/if}}
6182
+ {{#if modelTopP}}
6183
+ params["top_p"] = {{modelTopP}}
6184
+ {{/if}}
6185
+ {{#if (eq mantleApiFormat "chat_completions")}}
6186
+ return OpenAIModel(client_args=client_args, model_id=MODEL_ID, params=params)
6187
+ {{else}}
6188
+ # Responses API: Mantle does not persist responses, so disable server-side storage.
6189
+ params["store"] = False
6190
+ {{#if mantleProprietary}}
6191
+ return OpenAIResponsesModel(client_args=client_args, model_id=MODEL_ID, params=params)
6192
+ {{else}}
6193
+ return MantleCompatResponsesModel(client_args=client_args, model_id=MODEL_ID, params=params)
6194
+ {{/if}}
6195
+ {{/if}}
6196
+ {{else}}
4933
6197
  from strands.models.bedrock import BedrockModel
4934
6198
 
4935
6199
 
4936
6200
  def load_model() -> BedrockModel:
4937
6201
  """Get Bedrock model client using IAM credentials."""
4938
- return BedrockModel(model_id="global.anthropic.claude-sonnet-4-5-20250929-v1:0")
6202
+ return BedrockModel(model_id="{{#if modelId}}{{modelId}}{{else}}global.anthropic.claude-sonnet-4-5-20250929-v1:0{{/if}}")
6203
+ {{/if}}
4939
6204
  {{/if}}
4940
6205
  {{#if (eq modelProvider "Anthropic")}}
4941
6206
  import os
@@ -5011,7 +6276,7 @@ def load_model() -> OpenAIModel:
5011
6276
  """Get authenticated OpenAI model client."""
5012
6277
  return OpenAIModel(
5013
6278
  client_args={"api_key": _get_api_key()},
5014
- model_id="gpt-4.1",
6279
+ model_id="{{#if modelId}}{{modelId}}{{else}}gpt-4.1{{/if}}",
5015
6280
  )
5016
6281
  {{/if}}
5017
6282
  {{#if (eq modelProvider "Gemini")}}
@@ -5049,11 +6314,90 @@ def load_model() -> GeminiModel:
5049
6314
  """Get authenticated Gemini model client."""
5050
6315
  return GeminiModel(
5051
6316
  client_args={"api_key": _get_api_key()},
5052
- model_id="gemini-2.5-flash",
6317
+ model_id="{{#if modelId}}{{modelId}}{{else}}gemini-2.5-flash{{/if}}",
5053
6318
  )
5054
6319
  {{/if}}
5055
- "
5056
- `;
6320
+ {{#if (eq modelProvider "LiteLLM")}}
6321
+ import os
6322
+ {{#if litellmAdditionalParams}}
6323
+ import json
6324
+ {{/if}}
6325
+
6326
+ from strands.models.litellm import LiteLLMModel
6327
+ {{#if identityProviders.[0].name}}
6328
+ from bedrock_agentcore.identity.auth import requires_api_key
6329
+
6330
+ IDENTITY_PROVIDER_NAME = "{{identityProviders.[0].name}}"
6331
+ IDENTITY_ENV_VAR = "{{identityProviders.[0].envVarName}}"
6332
+
6333
+
6334
+ @requires_api_key(provider_name=IDENTITY_PROVIDER_NAME)
6335
+ def _agentcore_identity_api_key_provider(api_key: str) -> str:
6336
+ """Fetch API key from AgentCore Identity."""
6337
+ return api_key
6338
+
6339
+
6340
+ def _get_api_key() -> str:
6341
+ """
6342
+ Uses AgentCore Identity for API key management in deployed environments.
6343
+ For local development, run via 'agentcore dev' which loads agentcore/.env.
6344
+ """
6345
+ if os.getenv("LOCAL_DEV") == "1":
6346
+ api_key = os.getenv(IDENTITY_ENV_VAR)
6347
+ if not api_key:
6348
+ raise RuntimeError(
6349
+ f"{IDENTITY_ENV_VAR} not found. Add {IDENTITY_ENV_VAR}=your-key to .env.local"
6350
+ )
6351
+ return api_key
6352
+ return _agentcore_identity_api_key_provider()
6353
+ {{/if}}
6354
+
6355
+
6356
+
6357
+
6358
+ def load_model() -> LiteLLMModel:
6359
+ """Get a LiteLLM model client (proxies to the provider encoded in model_id)."""
6360
+ client_args = {}
6361
+ {{#if identityProviders.[0].name}}
6362
+ client_args["api_key"] = _get_api_key()
6363
+ {{/if}}
6364
+ {{#if litellmApiBase}}
6365
+ client_args["api_base"] = {{safeJson litellmApiBase}}
6366
+ {{/if}}
6367
+ params = {{#if litellmAdditionalParams}}json.loads({{pyJsonStr litellmAdditionalParams}}){{else}}{}{{/if}}
6368
+ return LiteLLMModel(
6369
+ client_args=client_args,
6370
+ model_id="{{#if modelId}}{{modelId}}{{else}}bedrock/us.anthropic.claude-sonnet-4-5-20250514-v1:0{{/if}}",
6371
+ params=params,
6372
+ )
6373
+ {{/if}}
6374
+ "
6375
+ `;
6376
+
6377
+ exports[`Assets Directory Snapshots > Python framework assets > python/python/http/strands/base/model/mantle_compat.py should match snapshot 1`] = `
6378
+ "from strands.models.openai_responses import OpenAIResponsesModel
6379
+
6380
+
6381
+ class MantleCompatResponsesModel(OpenAIResponsesModel):
6382
+ """Workaround for Bedrock Mantle rejecting output_text in EasyInputMessage content arrays.
6383
+
6384
+ Mantle's Pydantic validation only accepts content as a plain string for assistant messages, while
6385
+ real OpenAI accepts both formats. Flatten assistant content arrays to strings so multi-turn works.
6386
+ Used for open-source OpenAI models (gpt-oss-*) on the /v1 Mantle path; proprietary models use the
6387
+ plain OpenAIResponsesModel on /openai/v1.
6388
+ """
6389
+
6390
+ @classmethod
6391
+ def _format_request_messages(cls, messages):
6392
+ formatted = super()._format_request_messages(messages)
6393
+ for msg in formatted:
6394
+ if msg.get("role") == "assistant" and isinstance(msg.get("content"), list):
6395
+ msg["content"] = "".join(
6396
+ part.get("text", "") for part in msg["content"] if part.get("type") == "output_text"
6397
+ )
6398
+ return formatted
6399
+ "
6400
+ `;
5057
6401
 
5058
6402
  exports[`Assets Directory Snapshots > Python framework assets > python/python/http/strands/base/pyproject.toml should match snapshot 1`] = `
5059
6403
  "[build-system]
@@ -5069,13 +6413,19 @@ requires-python = ">=3.10"
5069
6413
  dependencies = [
5070
6414
  {{#if (eq modelProvider "Anthropic")}}"anthropic >= 0.30.0",
5071
6415
  {{/if}}"aws-opentelemetry-distro",
5072
- "bedrock-agentcore >= 1.0.3",
6416
+ "bedrock-agentcore >= 1.9.1",
5073
6417
  "botocore[crt] >= 1.35.0",
5074
6418
  {{#if (eq modelProvider "Gemini")}}"google-genai >= 1.0.0",
5075
6419
  {{/if}}"mcp >= 1.19.0",
5076
6420
  {{#if (eq modelProvider "OpenAI")}}"openai >= 1.0.0",
5077
- {{/if}}"strands-agents >= 1.13.0",
5078
- {{#if hasGateway}}{{#if (includes gatewayAuthTypes "AWS_IAM")}}"mcp-proxy-for-aws >= 1.1.0",
6421
+ {{/if}}{{#if (eq modelProvider "LiteLLM")}}"litellm >= 1.0.0",
6422
+ {{/if}}{{#if bedrockMantle}}"openai >= 1.0.0",
6423
+ "aws-bedrock-token-generator >= 1.0.0",
6424
+ {{/if}}"strands-agents >= 1.15.0",
6425
+ {{#if (or hasBrowser hasCodeInterpreter)}}"strands-agents-tools >= 0.1.0",
6426
+ {{/if}}{{#if hasBrowser}}"nest-asyncio >= 1.5.0",
6427
+ "playwright >= 1.42.0",
6428
+ {{/if}}{{#if hasGateway}}{{#if (includes gatewayAuthTypes "AWS_IAM")}}"mcp-proxy-for-aws >= 1.1.0",
5079
6429
  {{/if}}{{/if}}
5080
6430
  ]
5081
6431
 
@@ -5084,6 +6434,347 @@ packages = ["."]
5084
6434
  "
5085
6435
  `;
5086
6436
 
6437
+ exports[`Assets Directory Snapshots > Python framework assets > python/python/http/strands/base/skills/fetcher.py should match snapshot 1`] = `
6438
+ """"Skill fetcher — downloads s3/git skills to local filesystem on first use.
6439
+
6440
+ Resolved paths are passed to AgentSkills(skills=...) in main.py.
6441
+ Cache directory: <tmpdir>/.agents/skills/ — an absolute path under the system temp
6442
+ directory (honors $TMPDIR, defaults to /tmp). The runtime working directory (e.g.
6443
+ /var/task in a CodeZip runtime) is read-only, so the cache must live somewhere
6444
+ guaranteed-writable.
6445
+ """
6446
+
6447
+ import base64
6448
+ import hashlib
6449
+ import json
6450
+ import logging
6451
+ import os
6452
+ import shutil
6453
+ import subprocess
6454
+ import tempfile
6455
+ from pathlib import Path
6456
+ from typing import Optional
6457
+
6458
+ logger = logging.getLogger(__name__)
6459
+
6460
+ _SKILLS_BASE = Path(tempfile.gettempdir()) / ".agents" / "skills"
6461
+ _GIT_TIMEOUT = 60
6462
+ _S3_MAX_SIZE_BYTES = 1 * 1024 * 1024 * 1024 # 1 GB
6463
+
6464
+
6465
+ def _stable_hash(value: str) -> str:
6466
+ return hashlib.sha256(value.encode()).hexdigest()[:12]
6467
+
6468
+
6469
+ def _cleanup(path: Path) -> None:
6470
+ """Remove a partially-created skill directory so retries don't see stale state."""
6471
+ shutil.rmtree(path, ignore_errors=True)
6472
+
6473
+
6474
+ def _read_map(type_dir: Path) -> dict:
6475
+ map_file = type_dir / ".map.json"
6476
+ return json.loads(map_file.read_text()) if map_file.exists() else {}
6477
+
6478
+
6479
+ def _write_map(type_dir: Path, mapping: dict) -> None:
6480
+ type_dir.mkdir(parents=True, exist_ok=True)
6481
+ (type_dir / ".map.json").write_text(json.dumps(mapping))
6482
+
6483
+
6484
+ def _resolve_cached(type_dir: Path, source_hash: str) -> Optional[str]:
6485
+ """Return the cached skill directory for a source hash, or None if not on disk."""
6486
+ mapping = _read_map(type_dir)
6487
+ dir_name = mapping.get(source_hash)
6488
+ if dir_name and (type_dir / dir_name).exists():
6489
+ return str(type_dir / dir_name)
6490
+ return None
6491
+
6492
+
6493
+ def _read_skill_name(skill_dir: Path) -> str:
6494
+ """Extract the skill name from SKILL.md YAML frontmatter."""
6495
+ content = (skill_dir / "SKILL.md").read_text()
6496
+ if not content.startswith("---"):
6497
+ raise ValueError(f"SKILL.md in {skill_dir} has no YAML frontmatter (must start with ---)")
6498
+ parts = content.split("---", 2)
6499
+ if len(parts) < 3:
6500
+ raise ValueError(f"SKILL.md in {skill_dir} has malformed frontmatter (missing closing ---)")
6501
+ for line in parts[1].strip().splitlines():
6502
+ if line.startswith("name:"):
6503
+ name = line[len("name:"):].strip().strip("\\"'")
6504
+ if name:
6505
+ return name
6506
+ raise ValueError(f"SKILL.md in {skill_dir} is missing a 'name' field in frontmatter")
6507
+
6508
+
6509
+ def _pick_dir_name(type_dir: Path, name: str, source_hash: str) -> str:
6510
+ """Pick a unique directory name, appending a hash suffix on collision."""
6511
+ if not (type_dir / name).exists():
6512
+ return name
6513
+ return f"{name}-{source_hash[:8]}"
6514
+
6515
+
6516
+ def _rename_and_cache_skill(type_dir: Path, temp_dir: Path, source_hash: str, skill_root: Path,
6517
+ source_label: str = "") -> Path:
6518
+ """Validate SKILL.md, rename the temp dir to the skill's declared name, and update the map.
6519
+
6520
+ Raises ValueError if SKILL.md is missing or has invalid frontmatter.
6521
+ """
6522
+ if not (skill_root / "SKILL.md").exists():
6523
+ _cleanup(temp_dir)
6524
+ hint = f" (source: {source_label})" if source_label else ""
6525
+ raise ValueError(f"No SKILL.md found in fetched skill{hint}")
6526
+
6527
+ name = _read_skill_name(skill_root)
6528
+ dir_name = _pick_dir_name(type_dir, name, source_hash)
6529
+ final_dir = type_dir / dir_name
6530
+ if final_dir != temp_dir:
6531
+ temp_dir.rename(final_dir)
6532
+
6533
+ mapping = _read_map(type_dir)
6534
+ mapping[source_hash] = dir_name
6535
+ _write_map(type_dir, mapping)
6536
+ return final_dir
6537
+
6538
+
6539
+ def _fetch_s3_skill(source: str, s3_client=None) -> Path:
6540
+ """Download an s3:// skill prefix and return the local directory."""
6541
+ uri = source if source.endswith("/") else source + "/"
6542
+ source_hash = _stable_hash(uri)
6543
+ type_dir = _SKILLS_BASE / "s3"
6544
+
6545
+ cached = _resolve_cached(type_dir, source_hash)
6546
+ if cached:
6547
+ return Path(cached)
6548
+
6549
+ import boto3
6550
+ client = s3_client or boto3.client("s3")
6551
+ bucket, _, prefix = uri[len("s3://"):].partition("/")
6552
+ if not bucket:
6553
+ raise ValueError(f"Invalid S3 URI (no bucket): {uri}")
6554
+
6555
+ temp_dir = type_dir / source_hash
6556
+ _cleanup(temp_dir)
6557
+ temp_dir.mkdir(parents=True, exist_ok=True)
6558
+ temp_root = temp_dir.resolve()
6559
+
6560
+ paginator = client.get_paginator("list_objects_v2")
6561
+ total = 0
6562
+ for page in paginator.paginate(Bucket=bucket, Prefix=prefix):
6563
+ for obj in page.get("Contents", []):
6564
+ total += obj["Size"]
6565
+ if total > _S3_MAX_SIZE_BYTES:
6566
+ _cleanup(temp_dir)
6567
+ raise ValueError(f"S3 skill {uri} exceeds 1 GB size limit")
6568
+ rel = obj["Key"][len(prefix):].lstrip("/")
6569
+ if not rel:
6570
+ continue
6571
+ dest = (temp_dir / rel).resolve()
6572
+ if dest != temp_root and not str(dest).startswith(str(temp_root) + os.sep):
6573
+ _cleanup(temp_dir)
6574
+ raise ValueError(f"Path traversal detected in S3 key: {obj['Key']}")
6575
+ dest.parent.mkdir(parents=True, exist_ok=True)
6576
+ client.download_file(bucket, obj["Key"], str(dest))
6577
+
6578
+ if total == 0:
6579
+ _cleanup(temp_dir)
6580
+ raise ValueError(f"No files found at S3 URI: {uri}")
6581
+
6582
+ return _rename_and_cache_skill(type_dir, temp_dir, source_hash, temp_dir, source_label=uri)
6583
+
6584
+
6585
+ def _resolve_credential_arn(credential_arn: str, identity_client) -> str:
6586
+ """Resolve a Token Vault API-key credential ARN to its secret value via AgentCore Identity.
6587
+
6588
+ ARN format: arn:<p>:bedrock-agentcore:<region>:<account>:token-vault/<vault>/apikeycredentialprovider/<name>
6589
+ """
6590
+ from bedrock_agentcore.runtime.context import BedrockAgentCoreContext # noqa: PLC0415
6591
+
6592
+ provider_name = credential_arn.rsplit("/", 1)[-1]
6593
+ if not provider_name:
6594
+ raise ValueError(f"Invalid credential ARN: {credential_arn}")
6595
+ workload_token = BedrockAgentCoreContext.get_workload_access_token()
6596
+ if not workload_token:
6597
+ raise ValueError("Credential ARN resolution requires a workload access token")
6598
+ api_key = identity_client.dp_client.get_resource_api_key(
6599
+ resourceCredentialProviderName=provider_name,
6600
+ workloadIdentityToken=workload_token,
6601
+ )["apiKey"]
6602
+ if not api_key:
6603
+ raise ValueError(f"Identity returned empty API key for provider: {provider_name}")
6604
+ return api_key
6605
+
6606
+
6607
+ def _build_git_auth_env(credential_arn: Optional[str], username: Optional[str], identity_client=None) -> dict:
6608
+ """Build GIT_CONFIG_* env vars for HTTP Basic auth using a Token Vault credential ARN.
6609
+
6610
+ Uses env vars instead of -c args to avoid leaking credentials in /proc/*/cmdline,
6611
+ and so auth propagates to sub-commands (e.g. sparse-checkout triggering a fetch).
6612
+ """
6613
+ if not credential_arn or not identity_client:
6614
+ return {}
6615
+ password = _resolve_credential_arn(credential_arn, identity_client)
6616
+ user = username or "oauth2"
6617
+ encoded = base64.b64encode(f"{user}:{password}".encode()).decode()
6618
+ return {
6619
+ "GIT_CONFIG_COUNT": "1",
6620
+ "GIT_CONFIG_KEY_0": "http.extraHeader",
6621
+ "GIT_CONFIG_VALUE_0": f"Authorization: Basic {encoded}",
6622
+ }
6623
+
6624
+
6625
+ def _fetch_git_skill(url: str, skill_path: str = "", credential_arn: Optional[str] = None,
6626
+ username: Optional[str] = None, identity_client=None) -> Path:
6627
+ """Shallow-clone a git skill repository and return the local skill directory.
6628
+
6629
+ Returns the directory containing SKILL.md (the subdir itself for sparse checkouts).
6630
+ """
6631
+ if skill_path and (os.path.isabs(skill_path) or ".." in Path(skill_path).parts):
6632
+ raise ValueError(f"Path traversal detected in skill path: {skill_path}")
6633
+
6634
+ source_hash = _stable_hash(f"{url}:{skill_path}")
6635
+ type_dir = _SKILLS_BASE / "git"
6636
+
6637
+ cached = _resolve_cached(type_dir, source_hash)
6638
+ if cached:
6639
+ return Path(cached) / skill_path if skill_path else Path(cached)
6640
+
6641
+ temp_dir = type_dir / source_hash
6642
+ _cleanup(temp_dir)
6643
+ temp_dir.mkdir(parents=True, exist_ok=True)
6644
+
6645
+ extra_env = _build_git_auth_env(credential_arn, username, identity_client)
6646
+ git_env = {**os.environ, **extra_env} if extra_env else None
6647
+
6648
+ try:
6649
+ if skill_path:
6650
+ subprocess.run(
6651
+ ["git", "clone", "--depth", "1", "--filter=blob:none", "--sparse", url, str(temp_dir)],
6652
+ check=True, timeout=_GIT_TIMEOUT, capture_output=True, env=git_env,
6653
+ )
6654
+ subprocess.run(
6655
+ ["git", "sparse-checkout", "set", skill_path],
6656
+ check=True, timeout=_GIT_TIMEOUT, capture_output=True, cwd=str(temp_dir), env=git_env,
6657
+ )
6658
+ else:
6659
+ subprocess.run(
6660
+ ["git", "clone", "--depth", "1", url, str(temp_dir)],
6661
+ check=True, timeout=_GIT_TIMEOUT, capture_output=True, env=git_env,
6662
+ )
6663
+ except Exception:
6664
+ _cleanup(temp_dir)
6665
+ raise
6666
+
6667
+ if skill_path and not (temp_dir / skill_path).exists():
6668
+ _cleanup(temp_dir)
6669
+ raise ValueError(f"Skill path '{skill_path}' not found in repository '{url}'")
6670
+
6671
+ # SKILL.md lives inside the subdir for sparse checkouts.
6672
+ skill_root = temp_dir / skill_path if skill_path else temp_dir
6673
+ label = f"{url}:{skill_path}" if skill_path else url
6674
+ final_dir = _rename_and_cache_skill(type_dir, temp_dir, source_hash, skill_root, source_label=label)
6675
+ return final_dir / skill_path if skill_path else final_dir
6676
+
6677
+
6678
+ def resolve_s3_skills(sources: list, s3_client=None) -> list:
6679
+ """Resolve s3:// skill URIs to local filesystem paths.
6680
+
6681
+ Any fetch failure raises and fails the invocation — a partial skill set
6682
+ would silently run the agent without capabilities the harness declared.
6683
+ """
6684
+ paths = []
6685
+ for uri in sources:
6686
+ try:
6687
+ skill_dir = _fetch_s3_skill(uri, s3_client)
6688
+ except Exception as e:
6689
+ raise ValueError(f"Failed to resolve S3 skill '{uri}': {e}") from e
6690
+ paths.append(str(skill_dir.resolve()))
6691
+ return paths
6692
+
6693
+
6694
+ def resolve_git_skills(sources: list, identity_client=None) -> list:
6695
+ """Resolve git skill dicts to local filesystem paths.
6696
+
6697
+ Each source is a dict with keys: url (required), path (optional),
6698
+ credentialArn (optional), username (optional).
6699
+
6700
+ Any fetch failure raises and fails the invocation — a partial skill set
6701
+ would silently run the agent without capabilities the harness declared.
6702
+ """
6703
+ paths = []
6704
+ for source in sources:
6705
+ try:
6706
+ skill_dir = _fetch_git_skill(
6707
+ url=source["url"],
6708
+ skill_path=source.get("path") or "",
6709
+ credential_arn=source.get("credentialArn"),
6710
+ username=source.get("username"),
6711
+ identity_client=identity_client,
6712
+ )
6713
+ except Exception as e:
6714
+ raise ValueError(f"Failed to resolve git skill '{source.get('url', source)}': {e}") from e
6715
+ paths.append(str(skill_dir.resolve()))
6716
+ return paths
6717
+ "
6718
+ `;
6719
+
6720
+ exports[`Assets Directory Snapshots > Python framework assets > python/python/http/strands/capabilities/execution-limits/hooks/execution_limits.py should match snapshot 1`] = `
6721
+ "import time
6722
+ from typing import Optional
6723
+
6724
+ from strands.hooks import BeforeModelCallEvent
6725
+ from strands.hooks.registry import HookProvider, HookRegistry
6726
+ from strands.types.exceptions import EventLoopException
6727
+
6728
+
6729
+ class ExecutionLimitExceeded(Exception):
6730
+ def __init__(self, message: str) -> None:
6731
+ super().__init__(message)
6732
+
6733
+
6734
+ class ExecutionLimitsHook(HookProvider):
6735
+ def __init__(
6736
+ self,
6737
+ max_iterations: Optional[int] = None,
6738
+ max_tokens: Optional[int] = None,
6739
+ timeout_seconds: Optional[float] = None,
6740
+ ) -> None:
6741
+ self._max_iterations = max_iterations
6742
+ self._max_tokens = max_tokens
6743
+ self._timeout_seconds = timeout_seconds
6744
+ self._iteration_count = 0
6745
+ self._start_time = time.monotonic()
6746
+
6747
+ def register_hooks(self, registry: HookRegistry, **kwargs) -> None:
6748
+ registry.add_callback(BeforeModelCallEvent, self._check_limits)
6749
+
6750
+ def _check_limits(self, event: BeforeModelCallEvent) -> None:
6751
+ self._iteration_count += 1
6752
+
6753
+ if self._max_iterations is not None and self._iteration_count > self._max_iterations:
6754
+ raise EventLoopException(
6755
+ ExecutionLimitExceeded(f"Max iterations exceeded: {self._max_iterations}")
6756
+ )
6757
+
6758
+ if self._timeout_seconds is not None:
6759
+ elapsed = time.monotonic() - self._start_time
6760
+ if elapsed > self._timeout_seconds:
6761
+ raise EventLoopException(
6762
+ ExecutionLimitExceeded(
6763
+ f"Timeout exceeded: {self._timeout_seconds}s (elapsed {elapsed:.1f}s)"
6764
+ )
6765
+ )
6766
+
6767
+ if self._max_tokens is not None:
6768
+ used = event.agent.event_loop_metrics.accumulated_usage.get("outputTokens", 0)
6769
+ if used >= self._max_tokens:
6770
+ raise EventLoopException(
6771
+ ExecutionLimitExceeded(
6772
+ f"Max output tokens exceeded: {used}/{self._max_tokens}"
6773
+ )
6774
+ )
6775
+ "
6776
+ `;
6777
+
5087
6778
  exports[`Assets Directory Snapshots > Python framework assets > python/python/http/strands/capabilities/memory/__init__.py should match snapshot 1`] = `
5088
6779
  "# Package marker
5089
6780
  "
@@ -5091,6 +6782,7 @@ exports[`Assets Directory Snapshots > Python framework assets > python/python/ht
5091
6782
 
5092
6783
  exports[`Assets Directory Snapshots > Python framework assets > python/python/http/strands/capabilities/memory/session.py should match snapshot 1`] = `
5093
6784
  "import os
6785
+ import uuid
5094
6786
  from typing import Optional
5095
6787
 
5096
6788
  from bedrock_agentcore.memory.integrations.strands.config import AgentCoreMemoryConfig{{#if memoryProviders.[0].strategies.length}}, RetrievalConfig{{/if}}
@@ -5099,10 +6791,14 @@ from bedrock_agentcore.memory.integrations.strands.session_manager import AgentC
5099
6791
  MEMORY_ID = os.getenv("{{memoryProviders.[0].envVarName}}")
5100
6792
  REGION = os.getenv("AWS_REGION")
5101
6793
 
5102
- def get_memory_session_manager(session_id: str, actor_id: str) -> Optional[AgentCoreMemorySessionManager]:
6794
+ def get_memory_session_manager(session_id: Optional[str], actor_id: str) -> Optional[AgentCoreMemorySessionManager]:
5103
6795
  if not MEMORY_ID:
5104
6796
  return None
5105
6797
 
6798
+ # AgentCoreMemoryConfig rejects None; OAuth/CUSTOM_JWT callers can reach us
6799
+ # without a runtime session header, so synthesize one when absent.
6800
+ session_id = session_id or uuid.uuid4().hex
6801
+
5106
6802
  {{#if memoryProviders.[0].strategies.length}}
5107
6803
  retrieval_config = {
5108
6804
  {{#if (includes memoryProviders.[0].strategies "SEMANTIC")}}
@@ -5111,8 +6807,11 @@ def get_memory_session_manager(session_id: str, actor_id: str) -> Optional[Agent
5111
6807
  {{#if (includes memoryProviders.[0].strategies "USER_PREFERENCE")}}
5112
6808
  f"/users/{actor_id}/preferences": RetrievalConfig(top_k=3, relevance_score=0.5),
5113
6809
  {{/if}}
6810
+ {{#if (includes memoryProviders.[0].strategies "EPISODIC")}}
6811
+ f"/episodes/{actor_id}/{session_id}": RetrievalConfig(top_k=5, relevance_score=0.5),
6812
+ {{/if}}
5114
6813
  {{#if (includes memoryProviders.[0].strategies "SUMMARIZATION")}}
5115
- f"/summaries/{actor_id}/{session_id}": RetrievalConfig(top_k=3, relevance_score=0.5),
6814
+ f"/summaries/{actor_id}": RetrievalConfig(top_k=3, relevance_score=0.5),
5116
6815
  {{/if}}
5117
6816
  }
5118
6817
  {{/if}}
@@ -5132,6 +6831,160 @@ def get_memory_session_manager(session_id: str, actor_id: str) -> Optional[Agent
5132
6831
  "
5133
6832
  `;
5134
6833
 
6834
+ exports[`Assets Directory Snapshots > Python framework assets > python/python/http/strands/capabilities/payments/__init__.py should match snapshot 1`] = `
6835
+ """"Payment capabilities for Strands agents."""
6836
+ from .payments import create_payments_plugin
6837
+
6838
+ __all__ = ["create_payments_plugin"]
6839
+ "
6840
+ `;
6841
+
6842
+ exports[`Assets Directory Snapshots > Python framework assets > python/python/http/strands/capabilities/payments/payments.py should match snapshot 1`] = `
6843
+ """"Payment capability -- auto-generated by agentcore CLI.
6844
+
6845
+ Configures AgentCorePaymentsPlugin for Strands agents.
6846
+ Manager config is auto-discovered from AGENTCORE_PAYMENT_*
6847
+ environment variables set at deploy time.
6848
+
6849
+ Uses a per-invocation factory pattern so each request gets its own
6850
+ plugin instance with the correct user_id, instrument_id, and session_id.
6851
+ This prevents concurrency bugs where one user's payment context
6852
+ could leak to another user's request.
6853
+
6854
+ Uses the official SDK plugin which handles:
6855
+ - x402 v1 (body-based) and v2 (header-based) payment detection
6856
+ - Automatic 402 response interception and payment processing
6857
+ - Retry limiting (max 3 payment retries per tool use)
6858
+ - Error management and logging
6859
+ """
6860
+ import os
6861
+ import logging
6862
+
6863
+ import boto3
6864
+ from bedrock_agentcore.payments.integrations.strands import AgentCorePaymentsPlugin
6865
+ from bedrock_agentcore.payments.integrations.config import AgentCorePaymentsPluginConfig
6866
+
6867
+ logger = logging.getLogger(__name__)
6868
+
6869
+ PAYMENT_SYSTEM_PROMPT = """
6870
+ You have payment capabilities via the x402 protocol:
6871
+ - Use http_request to call HTTP endpoints. 402 Payment Required responses are settled automatically by the plugin and the call is retried.
6872
+ - Use get_payment_session to check your remaining budget before expensive operations
6873
+ - Use get_payment_instrument_balance to check wallet USDC balance
6874
+ - Use list_payment_instruments to see available payment instruments
6875
+ - If budget is low, inform the user before proceeding with paid requests
6876
+ """
6877
+
6878
+ _manager_arn = None
6879
+ _connector_id = None
6880
+ _process_payment_role_arn = None
6881
+ _name_segment = None
6882
+ _region = None
6883
+ _auth_mode = None
6884
+ _manager_count = 0
6885
+ for key, value in os.environ.items():
6886
+ if key.startswith("AGENTCORE_PAYMENT_") and key.endswith("_MANAGER_ARN"):
6887
+ if _manager_arn is None:
6888
+ _manager_arn = value
6889
+ _name_segment = key[len("AGENTCORE_PAYMENT_"):-len("_MANAGER_ARN")]
6890
+ _manager_count += 1
6891
+ if _manager_count > 1:
6892
+ logger.warning(
6893
+ "Multiple payment managers detected in environment. Using the first one found. "
6894
+ "Remove extra AGENTCORE_PAYMENT_*_MANAGER_ARN env vars to eliminate ambiguity."
6895
+ )
6896
+ _region = os.getenv("AWS_REGION")
6897
+
6898
+ _prefix = f"AGENTCORE_PAYMENT_{_name_segment}_" if _name_segment else "AGENTCORE_PAYMENT_"
6899
+ _auth_mode = os.getenv(f"{_prefix}AUTH_MODE", "sigv4")
6900
+ _connector_id = os.getenv(f"{_prefix}CONNECTOR_ID")
6901
+ _process_payment_role_arn = os.getenv(f"{_prefix}PROCESS_PAYMENT_ROLE_ARN")
6902
+ _auto_payment = os.getenv(f"{_prefix}AUTO_PAYMENT", "true").lower() == "true"
6903
+ _allowlist_raw = os.getenv(f"{_prefix}TOOL_ALLOWLIST")
6904
+ _allowlist = _allowlist_raw.split(",") if _allowlist_raw else None
6905
+ _network_prefs_raw = os.getenv(f"{_prefix}NETWORK_PREFERENCES")
6906
+ _network_prefs = _network_prefs_raw.split(",") if _network_prefs_raw else None
6907
+
6908
+ if not _manager_arn:
6909
+ logger.warning("No payment manager config found in environment")
6910
+ if not _connector_id:
6911
+ logger.warning("No payment connector config found in environment")
6912
+
6913
+
6914
+ def _assume_role_session(role_arn):
6915
+ """Assume an IAM role and return a boto3 session with temporary credentials."""
6916
+ sts = boto3.client("sts", region_name=_region)
6917
+ creds = sts.assume_role(
6918
+ RoleArn=role_arn,
6919
+ RoleSessionName="agentcore-payment-plugin",
6920
+ )["Credentials"]
6921
+ return boto3.Session(
6922
+ aws_access_key_id=creds["AccessKeyId"],
6923
+ aws_secret_access_key=creds["SecretAccessKey"],
6924
+ aws_session_token=creds["SessionToken"],
6925
+ region_name=_region,
6926
+ )
6927
+
6928
+
6929
+ def create_payments_plugin(user_id, instrument_id=None, session_id=None):
6930
+ """Create a fresh plugin instance per invocation.
6931
+
6932
+ Args:
6933
+ user_id: From invocation context (required for SigV4, derived from JWT for bearer)
6934
+ instrument_id: From invocation payload (created by app backend per user)
6935
+ session_id: From invocation payload (created by app backend per conversation)
6936
+
6937
+ Returns:
6938
+ AgentCorePaymentsPlugin instance, or None if no manager is configured.
6939
+ """
6940
+ if not _manager_arn:
6941
+ return None
6942
+
6943
+ config_kwargs = {
6944
+ "payment_manager_arn": _manager_arn,
6945
+ "region": _region,
6946
+ "payment_instrument_id": instrument_id,
6947
+ "payment_session_id": session_id,
6948
+ "payment_connector_id": _connector_id,
6949
+ }
6950
+
6951
+ config_kwargs["auto_payment"] = _auto_payment
6952
+ if _allowlist:
6953
+ config_kwargs["payment_tool_allowlist"] = _allowlist
6954
+ if _network_prefs:
6955
+ config_kwargs["network_preferences_config"] = _network_prefs
6956
+
6957
+ if _process_payment_role_arn:
6958
+ # Only pass boto3_session if SDK supports it (added in bedrock-agentcore >= 1.11).
6959
+ # Older SDKs use the runtime role's default credentials and can still call ProcessPayment
6960
+ # if the runtime role has been granted permission directly.
6961
+ import inspect
6962
+ if "boto3_session" in inspect.signature(AgentCorePaymentsPluginConfig).parameters:
6963
+ config_kwargs["boto3_session"] = _assume_role_session(_process_payment_role_arn)
6964
+ else:
6965
+ logger.warning(
6966
+ "PROCESS_PAYMENT_ROLE_ARN set but bedrock-agentcore SDK does not support boto3_session. "
6967
+ "Upgrade to bedrock-agentcore>=1.11 to enable cross-role payment processing."
6968
+ )
6969
+
6970
+ if _auth_mode == "bearer":
6971
+ bearer_token = os.getenv("AGENTCORE_BEARER_TOKEN")
6972
+ if bearer_token:
6973
+ config_kwargs["bearer_token"] = bearer_token
6974
+ else:
6975
+ logger.warning(
6976
+ "Bearer auth mode configured but AGENTCORE_BEARER_TOKEN not set. "
6977
+ "Falling back to SigV4. Set AGENTCORE_BEARER_TOKEN or pass bearer_token in invoke context."
6978
+ )
6979
+ config_kwargs["user_id"] = user_id or "default-user"
6980
+ else:
6981
+ config_kwargs["user_id"] = user_id or "default-user"
6982
+
6983
+ config = AgentCorePaymentsPluginConfig(**config_kwargs)
6984
+ return AgentCorePaymentsPlugin(config=config)
6985
+ "
6986
+ `;
6987
+
5135
6988
  exports[`Assets Directory Snapshots > Python framework assets > python/python/mcp/standalone/base/README.md should match snapshot 1`] = `
5136
6989
  "# {{ name }}
5137
6990
 
@@ -5218,10 +7071,68 @@ Thumbs.db
5218
7071
  `;
5219
7072
 
5220
7073
  exports[`Assets Directory Snapshots > Python framework assets > python/python/mcp/standalone/base/main.py should match snapshot 1`] = `
5221
- "from mcp.server.fastmcp import FastMCP
7074
+ "{{#if needsOs}}
7075
+ import os
7076
+ {{/if}}
7077
+ from mcp.server.fastmcp import FastMCP
5222
7078
 
5223
7079
  mcp = FastMCP("{{ name }}", host="0.0.0.0", stateless_http=True)
5224
7080
 
7081
+ {{#if needsOs}}
7082
+ _MOUNT_PATHS = [
7083
+ {{#if sessionStorageMountPath}}"{{sessionStorageMountPath}}",{{/if}}
7084
+ {{#each efsMounts}}"{{mountPath}}",{{/each}}
7085
+ {{#each s3Mounts}}"{{mountPath}}",{{/each}}
7086
+ ]
7087
+
7088
+ def _safe_resolve(path: str) -> str:
7089
+ resolved = os.path.realpath(path)
7090
+ if not any(resolved == os.path.realpath(m) or resolved.startswith(os.path.realpath(m) + os.sep) for m in _MOUNT_PATHS):
7091
+ raise ValueError(f"Path '{path}' is not within any configured mount ({', '.join(_MOUNT_PATHS)})")
7092
+ return resolved
7093
+
7094
+ @mcp.tool()
7095
+ def file_read(path: str) -> str:
7096
+ """Read a file from a mounted filesystem. Use the absolute path (e.g. /mnt/tools/data.txt)."""
7097
+ try:
7098
+ full_path = _safe_resolve(path)
7099
+ with open(full_path) as f:
7100
+ return f.read()
7101
+ except ValueError as e:
7102
+ return str(e)
7103
+ except OSError as e:
7104
+ return f"Error reading '{path}': {e.strerror}"
7105
+
7106
+ @mcp.tool()
7107
+ def file_write(path: str, content: str) -> str:
7108
+ """Write a file to a mounted filesystem. Use the absolute path (e.g. /mnt/tools/data.txt)."""
7109
+ try:
7110
+ full_path = _safe_resolve(path)
7111
+ parent = os.path.dirname(full_path)
7112
+ if parent:
7113
+ os.makedirs(parent, exist_ok=True)
7114
+ with open(full_path, "w") as f:
7115
+ f.write(content)
7116
+ return f"Written to {path}"
7117
+ except ValueError as e:
7118
+ return str(e)
7119
+ except OSError as e:
7120
+ return f"Error writing '{path}': {e.strerror}"
7121
+
7122
+ @mcp.tool()
7123
+ def list_files(path: str) -> str:
7124
+ """List files in a mounted filesystem directory. Use the absolute path (e.g. /mnt/tools)."""
7125
+ try:
7126
+ full_path = _safe_resolve(path)
7127
+ entries = os.listdir(full_path)
7128
+ return "\\n".join(entries) if entries else "(empty directory)"
7129
+ except ValueError as e:
7130
+ return str(e)
7131
+ except OSError as e:
7132
+ return f"Error listing '{path}': {e.strerror}"
7133
+
7134
+ {{/if}}
7135
+
5225
7136
 
5226
7137
  @mcp.tool()
5227
7138
  def add_numbers(a: int, b: int) -> int:
@@ -5497,13 +7408,21 @@ file maps to a JSON config file and includes validation constraints as comments
5497
7408
 
5498
7409
  ### Key Types
5499
7410
 
5500
- - **AgentCoreProjectSpec**: Root config with \`runtimes\`, \`memories\`, \`credentials\`, \`agentCoreGateways\`, \`evaluators\`, \`onlineEvalConfigs\`, \`policyEngines\` arrays
7411
+ - **AgentCoreProjectSpec**: Root config with \`runtimes\`, \`memories\`, \`credentials\`, \`agentCoreGateways\`, \`evaluators\`, \`onlineEvalConfigs\`, \`onlineInsightsConfigs\`, \`knowledgeBases\`, \`harnesses\`, \`policyEngines\`, \`policies\`, \`payments\` (managers + connectors), \`configBundles\`, \`datasets\`, \`runtimeEndpoints\` arrays
5501
7412
  - **AgentEnvSpec**: Agent configuration (build type, entrypoint, code location, runtime version, network mode)
5502
7413
  - **Memory**: Memory resource with strategies (SEMANTIC, SUMMARIZATION, USER_PREFERENCE, EPISODIC) and expiry
5503
7414
  - **Credential**: API key or OAuth credential provider
5504
- - **AgentCoreGateway**: MCP gateway with targets (Lambda, MCP server, OpenAPI, Smithy, API Gateway)
7415
+ - **AgentCoreGateway**: MCP gateway with targets (Lambda, MCP server, OpenAPI, Smithy, API Gateway, web-search, knowledge-base)
5505
7416
  - **Evaluator**: LLM-as-a-Judge or code-based evaluator
5506
7417
  - **OnlineEvalConfig**: Continuous evaluation pipeline bound to an agent
7418
+ - **OnlineInsightsConfig** _[preview]_: Continuous failure-pattern analysis bound to an agent
7419
+ - **KnowledgeBase**: Managed Bedrock Knowledge Base auto-wired to a gateway
7420
+ - **Harness**: Declarative agent — runtime + tools + skills + memory + observability without writing agent code
7421
+ - **PolicyEngine** + **Policy**: Cedar policy engine with form-based guardrails (Bedrock content filters, prompt-attack, sensitive-info) or raw Cedar policies
7422
+ - **PaymentManager** + **PaymentConnector**: x402-protocol payment orchestration with provider credentials (CoinbaseCDP, StripePrivy)
7423
+ - **ConfigBundle**: Versioned runtime configuration as a separately-deployable resource
7424
+ - **Dataset**: Curated session dataset for batch evaluation and recommendation runs
7425
+ - **RuntimeEndpoint**: Named endpoint (e.g. \`PROMPT_V1\`) targeting a specific runtime version
5507
7426
 
5508
7427
  ### Common Enum Values
5509
7428
 
@@ -5511,8 +7430,11 @@ file maps to a JSON config file and includes validation constraints as comments
5511
7430
  - **NetworkMode**: \`'PUBLIC'\` | \`'VPC'\`
5512
7431
  - **RuntimeVersion**: \`'PYTHON_3_10'\` | \`'PYTHON_3_11'\` | \`'PYTHON_3_12'\` | \`'PYTHON_3_13'\` | \`'PYTHON_3_14'\` | \`'NODE_18'\` | \`'NODE_20'\` | \`'NODE_22'\`
5513
7432
  - **MemoryStrategyType**: \`'SEMANTIC'\` | \`'SUMMARIZATION'\` | \`'USER_PREFERENCE'\` | \`'EPISODIC'\`
5514
- - **GatewayTargetType**: \`'lambda'\` | \`'mcpServer'\` | \`'openApiSchema'\` | \`'smithyModel'\` | \`'apiGateway'\` | \`'lambdaFunctionArn'\`
7433
+ - **GatewayTargetType**: \`'lambda'\` | \`'mcpServer'\` | \`'openApiSchema'\` | \`'smithyModel'\` | \`'apiGateway'\` | \`'lambdaFunctionArn'\` | \`'connector'\` (web-search, bedrock-knowledge-bases)
5515
7434
  - **ModelProvider**: \`'Bedrock'\` | \`'Gemini'\` | \`'OpenAI'\` | \`'Anthropic'\`
7435
+ - **PaymentProvider**: \`'CoinbaseCDP'\` | \`'StripePrivy'\`
7436
+ - **PolicyEnforcementMode**: \`'ACTIVE'\` | \`'PASSIVE'\`
7437
+ - **GuardrailContentFilter**: \`'VIOLENCE'\` | \`'HATE'\` | \`'SEXUAL'\` | \`'MISCONDUCT'\` | \`'INSULTS'\`
5516
7438
 
5517
7439
  ### Build Types
5518
7440
 
@@ -5563,24 +7485,868 @@ When modifying JSON config files:
5563
7485
  4. Use CloudFormation-safe names (alphanumeric, start with letter)
5564
7486
  5. Run \`agentcore validate\` to verify changes
5565
7487
 
7488
+ ## Harness Export
7489
+
7490
+ \`agentcore export harness\` converts a harness configuration into a deployable Strands Python agent under \`app/<agentName>/\`.
7491
+
7492
+ **After every export, you MUST read \`app/<agentName>/EXPORT_NOTES.md\` before proceeding.**
7493
+
7494
+ 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\`.
7495
+
7496
+ \`\`\`bash
7497
+ agentcore export harness --name <harnessName> # generates app/<agentName>/EXPORT_NOTES.md
7498
+ cat app/<agentName>/EXPORT_NOTES.md # read this before touching anything else
7499
+ \`\`\`
7500
+
5566
7501
  ## CLI Commands
5567
7502
 
7503
+ Run \`agentcore --help\` or \`agentcore <command> --help\` for full flags. Commonly used:
7504
+
7505
+ **Project lifecycle**
7506
+
5568
7507
  | Command | Description |
5569
7508
  | --- | --- |
5570
7509
  | \`agentcore create\` | Create a new project |
5571
- | \`agentcore add <resource>\` | Add agent, memory, credential, gateway, evaluator, policy |
5572
- | \`agentcore remove <resource>\` | Remove a resource |
5573
7510
  | \`agentcore dev\` | Run agent locally with hot-reload |
5574
7511
  | \`agentcore deploy\` | Deploy to AWS |
5575
- | \`agentcore status\` | Show deployment status |
5576
7512
  | \`agentcore invoke\` | Invoke agent (local or deployed) |
5577
- | \`agentcore logs\` | View agent logs |
5578
- | \`agentcore traces\` | View agent traces |
5579
- | \`agentcore eval\` | Run evaluations against an agent |
5580
- | \`agentcore package\` | Package agent artifacts |
7513
+ | \`agentcore status\` | Show deployment status |
5581
7514
  | \`agentcore validate\` | Validate configuration |
5582
- | \`agentcore pause\` / \`resume\` | Pause or resume a deployed agent |
7515
+ | \`agentcore package\` | Package agent artifacts |
7516
+ | \`agentcore import\` | Import resources from a Bedrock AgentCore Starter Toolkit project |
7517
+
7518
+ **Resources**
7519
+
7520
+ | Command | Description |
7521
+ | --- | --- |
7522
+ | \`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, config-bundle, dataset, runtime-endpoint |
7523
+ | \`agentcore remove <resource>\` | Remove any resource |
7524
+ | \`agentcore export harness\` | Export a harness to a Strands runtime agent under \`app/<agentName>/\` |
7525
+
7526
+ **Jobs (run, view, archive, lifecycle)**
7527
+
7528
+ | Command | Description |
7529
+ | --- | --- |
7530
+ | \`agentcore run eval\` | Run on-demand evaluation against agent traces |
7531
+ | \`agentcore run batch-evaluation\` | Run evaluators across all sessions at scale |
7532
+ | \`agentcore run recommendation\` | Optimize prompts or tool descriptions from real traces |
7533
+ | \`agentcore run insights\` _[preview]_ | Run failure-pattern analysis across sessions |
7534
+ | \`agentcore run ab-test\` | Start an A/B test (config-bundle or target-based) |
7535
+ | \`agentcore run ingest\` | Start a fresh ingestion job for every data source on a deployed knowledge base |
7536
+ | \`agentcore view <type>\` | List or view jobs (recommendation, batch-evaluation, ab-test, insights) |
7537
+ | \`agentcore archive <type>\` | Delete a job on the service + clear local history |
7538
+ | \`agentcore stop <type>\` | Stop a running batch-evaluation or ab-test |
7539
+ | \`agentcore promote ab-test\` | Apply the winning variant to \`agentcore.json\` |
7540
+ | \`agentcore pause <type>\` / \`agentcore resume <type>\` | Pause/resume a deployed online-eval, online-insights, or ab-test |
7541
+
7542
+ **Config bundles & datasets**
7543
+
7544
+ | Command | Description |
7545
+ | --- | --- |
7546
+ | \`agentcore config-bundle versions\` (alias \`cb versions\`) | List version history for a bundle |
7547
+ | \`agentcore config-bundle diff\` | Diff two versions of a bundle |
7548
+ | \`agentcore config-bundle create-branch\` | Create a new branch on an existing bundle |
7549
+ | \`agentcore dataset download\` | Download a dataset version locally |
7550
+ | \`agentcore dataset publish-version\` | Publish a new dataset version |
7551
+ | \`agentcore dataset remove-version\` | Remove a dataset version |
7552
+
7553
+ **Observability & history**
7554
+
7555
+ | Command | Description |
7556
+ | --- | --- |
7557
+ | \`agentcore logs\` | Stream/search agent runtime logs |
7558
+ | \`agentcore logs evals\` | Stream/search online-eval logs |
7559
+ | \`agentcore traces list\` / \`agentcore traces get\` | List recent traces or download one to JSON |
7560
+ | \`agentcore evals history\` | View past on-demand eval results |
7561
+
7562
+ **Utilities**
7563
+
7564
+ | Command | Description |
7565
+ | --- | --- |
7566
+ | \`agentcore fetch access\` | Fetch access info for deployed gateway or agent |
7567
+ | \`agentcore feedback\` | Send feedback (with optional screenshot) to the AgentCore team |
7568
+ | \`agentcore update\` | Check for and install CLI updates |
7569
+ | \`agentcore telemetry\` | View or change telemetry preferences |
5583
7570
  "
5584
7571
  `;
5585
7572
 
5586
7573
  exports[`Assets Directory Snapshots > TypeScript assets > typescript/typescript/.gitkeep should match snapshot 1`] = `""`;
7574
+
7575
+ exports[`Assets Directory Snapshots > TypeScript assets > typescript/typescript/http/strands/base/README.md should match snapshot 1`] = `
7576
+ "This is a project generated by the AgentCore CLI!
7577
+
7578
+ # Layout
7579
+
7580
+ The generated application code lives at the agent root directory. At the root, there is a \`.gitignore\` file, an
7581
+ \`agentcore/\` folder which represents the configurations and state associated with this project. Other \`agentcore\`
7582
+ commands like \`deploy\`, \`dev\`, and \`invoke\` rely on the configuration stored here.
7583
+
7584
+ ## Agent Root
7585
+
7586
+ The main entrypoint to your app is defined in \`main.ts\`. Using the AgentCore SDK \`BedrockAgentCoreApp\`, this file
7587
+ defines an HTTP server that streams tokens from your chosen Agent framework SDK.
7588
+
7589
+ \`model/load.ts\` instantiates your chosen model provider.
7590
+
7591
+ ## Environment Variables
7592
+
7593
+ | Variable | Required | Description |
7594
+ | --- | --- | --- |
7595
+ {{#if hasIdentity}}| \`{{identityProviders.[0].envVarName}}\` | Yes | {{modelProvider}} API key (local) or Identity provider name (deployed) |
7596
+ {{/if}}| \`LOCAL_DEV\` | No | Set to \`1\` to use \`.env.local\` instead of AgentCore Identity |
7597
+
7598
+ # Developing locally
7599
+
7600
+ If installation was successful, \`node_modules/\` is already populated with dependencies.
7601
+
7602
+ \`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\`).
7603
+
7604
+ In a new terminal, you can invoke that server with:
7605
+
7606
+ \`agentcore invoke --dev "What can you do"\`
7607
+
7608
+ # Deployment
7609
+
7610
+ After providing credentials, \`agentcore deploy\` will deploy your project into Amazon Bedrock AgentCore.
7611
+
7612
+ Use \`agentcore invoke\` to invoke your deployed agent.
7613
+ "
7614
+ `;
7615
+
7616
+ exports[`Assets Directory Snapshots > TypeScript assets > typescript/typescript/http/strands/base/gitignore.template should match snapshot 1`] = `
7617
+ "# Environment variables
7618
+ .env
7619
+ .env.*
7620
+
7621
+ # Node
7622
+ node_modules/
7623
+ dist/
7624
+ *.log
7625
+ npm-debug.log*
7626
+ yarn-debug.log*
7627
+ yarn-error.log*
7628
+
7629
+ # IDE
7630
+ .vscode/
7631
+ .idea/
7632
+ *.swp
7633
+ *.swo
7634
+ *~
7635
+
7636
+ # OS
7637
+ .DS_Store
7638
+ Thumbs.db
7639
+ "
7640
+ `;
7641
+
7642
+ exports[`Assets Directory Snapshots > TypeScript assets > typescript/typescript/http/strands/base/main.ts should match snapshot 1`] = `
7643
+ "import { BedrockAgentCoreApp } from 'bedrock-agentcore/runtime';
7644
+ import { Agent, McpClient, tool, type ToolList } from '@strands-agents/sdk';
7645
+ import { z } from 'zod';
7646
+ import { loadModel } from './model/load.js';
7647
+ import { getStreamableHttpMcpClient } from './mcp_client/client.js';
7648
+ {{#if hasMemory}}
7649
+ import { getActorId, getOrCreateMemoryManager } from './memory/memory.js';
7650
+ {{/if}}
7651
+
7652
+ // Define a collection of MCP clients (filter out anything that failed to initialize)
7653
+ const mcpClients: McpClient[] = [getStreamableHttpMcpClient()].filter(
7654
+ (client): client is McpClient => Boolean(client)
7655
+ );
7656
+
7657
+ // Define a collection of tools used by the model
7658
+ const tools: ToolList = [];
7659
+
7660
+ // Define a simple function tool — the Zod schema gives us type inference and runtime validation for free
7661
+ const addNumbers = tool({
7662
+ name: 'add_numbers',
7663
+ description: 'Return the sum of two numbers',
7664
+ inputSchema: z.object({
7665
+ a: z.number(),
7666
+ b: z.number(),
7667
+ }),
7668
+ callback: async ({ a, b }) => a + b,
7669
+ });
7670
+ tools.push(addNumbers);
7671
+
7672
+ // Add MCP clients to tools
7673
+ tools.push(...mcpClients);
7674
+
7675
+ const SYSTEM_PROMPT = \`
7676
+ You are a helpful assistant. Use tools when appropriate.
7677
+ \`;
7678
+
7679
+ {{#if hasMemory}}
7680
+ const agentCache = new Map<string, Agent>();
7681
+
7682
+ async function getOrCreateAgent(sessionId: string, actorId: string): Promise<Agent> {
7683
+ const key = \`\${actorId}:\${sessionId}\`;
7684
+ let agent = agentCache.get(key);
7685
+ if (agent) return agent;
7686
+
7687
+ const model = await loadModel();
7688
+ agent = new Agent({
7689
+ model,
7690
+ systemPrompt: SYSTEM_PROMPT,
7691
+ tools,
7692
+ memoryManager: getOrCreateMemoryManager(sessionId, actorId) ?? undefined,
7693
+ });
7694
+ agentCache.set(key, agent);
7695
+ return agent;
7696
+ }
7697
+ {{else}}
7698
+ const AGENT_CACHE_LIMIT = 128;
7699
+
7700
+ // Reuses one Agent per sessionId so each session keeps its own in-process
7701
+ // conversation history (best-effort; resets on cold start). A Map preserves
7702
+ // insertion order, so it doubles as an LRU bounded to 128 sessions — a local
7703
+ // dev process serving many sessions cannot leak history between them or grow
7704
+ // without bound. On AgentCore Runtime each microVM serves a single session, so
7705
+ // this holds one entry. For durable history, attach memory.
7706
+ const agentCache = new Map<string, Agent>();
7707
+
7708
+ async function getOrCreateAgent(sessionId: string): Promise<Agent> {
7709
+ const existing = agentCache.get(sessionId);
7710
+ if (existing) {
7711
+ agentCache.delete(sessionId);
7712
+ agentCache.set(sessionId, existing);
7713
+ return existing;
7714
+ }
7715
+ if (agentCache.size >= AGENT_CACHE_LIMIT) {
7716
+ const oldest = agentCache.keys().next().value;
7717
+ if (oldest !== undefined) agentCache.delete(oldest);
7718
+ }
7719
+ const model = await loadModel();
7720
+ const agent = new Agent({
7721
+ model,
7722
+ systemPrompt: SYSTEM_PROMPT,
7723
+ tools,
7724
+ });
7725
+ agentCache.set(sessionId, agent);
7726
+ return agent;
7727
+ }
7728
+ {{/if}}
7729
+
7730
+ const app = new BedrockAgentCoreApp({
7731
+ invocationHandler: {
7732
+ async *process(payload: any, context: any) {
7733
+ {{#if hasMemory}}
7734
+ const sessionId = context?.sessionId ?? 'default-session';
7735
+ const actorId = getActorId(payload, context);
7736
+ const agent = await getOrCreateAgent(sessionId, actorId);
7737
+ {{else}}
7738
+ const sessionId = context?.sessionId ?? 'default-session';
7739
+ const agent = await getOrCreateAgent(sessionId);
7740
+ {{/if}}
7741
+
7742
+ {{#if hasMemory}}
7743
+ try {
7744
+ for await (const event of agent.stream(payload.prompt ?? '')) {
7745
+ if (
7746
+ event.type === 'modelStreamUpdateEvent' &&
7747
+ event.event?.type === 'modelContentBlockDeltaEvent' &&
7748
+ event.event.delta?.type === 'textDelta'
7749
+ ) {
7750
+ yield { data: event.event.delta.text };
7751
+ }
7752
+ }
7753
+ } finally {
7754
+ // Drain in-flight createEvent calls before the runtime can reclaim
7755
+ // the session microVM. flush() is the durability mechanism — without
7756
+ // it, an idle reclamation can lose the tail of the conversation.
7757
+ await agent.memoryManager?.flush();
7758
+ }
7759
+ {{else}}
7760
+ // Snapshot history before streaming so a failed turn can be rolled back.
7761
+ // Agent.stream() appends the user message before invoking the model; on a
7762
+ // mid-stream error that user turn would otherwise linger in the cached
7763
+ // agent, and the next turn for this session would send consecutive user
7764
+ // messages (rejected by providers that require strict role alternation,
7765
+ // e.g. Anthropic). Restoring on error keeps the session reusable.
7766
+ const snapshot = agent.takeSnapshot({ include: ['messages'] });
7767
+ try {
7768
+ for await (const event of agent.stream(payload.prompt ?? '')) {
7769
+ if (
7770
+ event.type === 'modelStreamUpdateEvent' &&
7771
+ event.event?.type === 'modelContentBlockDeltaEvent' &&
7772
+ event.event.delta?.type === 'textDelta'
7773
+ ) {
7774
+ yield { data: event.event.delta.text };
7775
+ }
7776
+ }
7777
+ } catch (error) {
7778
+ agent.loadSnapshot(snapshot);
7779
+ throw error;
7780
+ }
7781
+ {{/if}}
7782
+ },
7783
+ },
7784
+ });
7785
+
7786
+ app.run({ port: parseInt(process.env.PORT ?? '8080') });
7787
+ "
7788
+ `;
7789
+
7790
+ exports[`Assets Directory Snapshots > TypeScript assets > typescript/typescript/http/strands/base/mcp_client/client.ts should match snapshot 1`] = `
7791
+ "import { McpClient } from '@strands-agents/sdk';
7792
+ import { StreamableHTTPClientTransport } from '@modelcontextprotocol/sdk/client/streamableHttp.js';
7793
+
7794
+ // ExaAI provides information about code through web searches, crawling and code context searches through their platform. Requires no authentication
7795
+ const EXAMPLE_MCP_ENDPOINT = 'https://mcp.exa.ai/mcp';
7796
+
7797
+ export function getStreamableHttpMcpClient(): McpClient {
7798
+ // to use an MCP server that supports bearer authentication, add a headers() callback to requestInit
7799
+ const transport = new StreamableHTTPClientTransport(new URL(EXAMPLE_MCP_ENDPOINT));
7800
+ return new McpClient({ transport });
7801
+ }
7802
+ "
7803
+ `;
7804
+
7805
+ exports[`Assets Directory Snapshots > TypeScript assets > typescript/typescript/http/strands/base/model/load.ts should match snapshot 1`] = `
7806
+ "{{#if (eq modelProvider "Bedrock")}}
7807
+ import { BedrockModel } from '@strands-agents/sdk/models/bedrock';
7808
+
7809
+ export function loadModel(): BedrockModel {
7810
+ return new BedrockModel({ modelId: 'global.anthropic.claude-sonnet-4-5-20250929-v1:0' });
7811
+ }
7812
+ {{/if}}
7813
+ {{#if (eq modelProvider "Anthropic")}}
7814
+ import { AnthropicModel } from '@strands-agents/sdk/models/anthropic';
7815
+ import { withApiKey } from 'bedrock-agentcore/identity';
7816
+
7817
+ const IDENTITY_PROVIDER_NAME = '{{identityProviders.[0].name}}';
7818
+ const IDENTITY_ENV_VAR = '{{identityProviders.[0].envVarName}}';
7819
+
7820
+ async function getApiKey(): Promise<string> {
7821
+ if (process.env.LOCAL_DEV === '1') {
7822
+ const apiKey = process.env[IDENTITY_ENV_VAR] ?? process.env.ANTHROPIC_API_KEY;
7823
+ if (!apiKey) {
7824
+ throw new Error(\`\${IDENTITY_ENV_VAR} or ANTHROPIC_API_KEY not found. Add your key to agentcore/.env.local\`);
7825
+ }
7826
+ return apiKey;
7827
+ }
7828
+ return withApiKey({ providerName: IDENTITY_PROVIDER_NAME })(async (apiKey: string) => apiKey)();
7829
+ }
7830
+
7831
+ let _model: AnthropicModel | undefined;
7832
+
7833
+ export async function loadModel(): Promise<AnthropicModel> {
7834
+ if (!_model) {
7835
+ const apiKey = await getApiKey();
7836
+ _model = new AnthropicModel({
7837
+ apiKey,
7838
+ modelId: 'claude-sonnet-4-5-20250929',
7839
+ maxTokens: 5000,
7840
+ });
7841
+ }
7842
+ return _model;
7843
+ }
7844
+ {{/if}}
7845
+ {{#if (eq modelProvider "OpenAI")}}
7846
+ import { OpenAIModel } from '@strands-agents/sdk/models/openai';
7847
+ import { withApiKey } from 'bedrock-agentcore/identity';
7848
+
7849
+ const IDENTITY_PROVIDER_NAME = '{{identityProviders.[0].name}}';
7850
+ const IDENTITY_ENV_VAR = '{{identityProviders.[0].envVarName}}';
7851
+
7852
+ async function getApiKey(): Promise<string> {
7853
+ if (process.env.LOCAL_DEV === '1') {
7854
+ const apiKey = process.env[IDENTITY_ENV_VAR] ?? process.env.OPENAI_API_KEY;
7855
+ if (!apiKey) {
7856
+ throw new Error(\`\${IDENTITY_ENV_VAR} or OPENAI_API_KEY not found. Add your key to agentcore/.env.local\`);
7857
+ }
7858
+ return apiKey;
7859
+ }
7860
+ return withApiKey({ providerName: IDENTITY_PROVIDER_NAME })(async (apiKey: string) => apiKey)();
7861
+ }
7862
+
7863
+ let _model: OpenAIModel | undefined;
7864
+
7865
+ export async function loadModel(): Promise<OpenAIModel> {
7866
+ if (!_model) {
7867
+ const apiKey = await getApiKey();
7868
+ _model = new OpenAIModel({
7869
+ api: 'chat',
7870
+ apiKey,
7871
+ modelId: 'gpt-4.1',
7872
+ });
7873
+ }
7874
+ return _model;
7875
+ }
7876
+ {{/if}}
7877
+ {{#if (eq modelProvider "Gemini")}}
7878
+ import { GoogleModel } from '@strands-agents/sdk/models/google';
7879
+ import { withApiKey } from 'bedrock-agentcore/identity';
7880
+
7881
+ const IDENTITY_PROVIDER_NAME = '{{identityProviders.[0].name}}';
7882
+ const IDENTITY_ENV_VAR = '{{identityProviders.[0].envVarName}}';
7883
+
7884
+ async function getApiKey(): Promise<string> {
7885
+ if (process.env.LOCAL_DEV === '1') {
7886
+ const apiKey = process.env[IDENTITY_ENV_VAR] ?? process.env.GEMINI_API_KEY;
7887
+ if (!apiKey) {
7888
+ throw new Error(\`\${IDENTITY_ENV_VAR} or GEMINI_API_KEY not found. Add your key to agentcore/.env.local\`);
7889
+ }
7890
+ return apiKey;
7891
+ }
7892
+ return withApiKey({ providerName: IDENTITY_PROVIDER_NAME })(async (apiKey: string) => apiKey)();
7893
+ }
7894
+
7895
+ let _model: GoogleModel | undefined;
7896
+
7897
+ export async function loadModel(): Promise<GoogleModel> {
7898
+ if (!_model) {
7899
+ const apiKey = await getApiKey();
7900
+ _model = new GoogleModel({
7901
+ apiKey,
7902
+ modelId: 'gemini-2.5-flash',
7903
+ });
7904
+ }
7905
+ return _model;
7906
+ }
7907
+ {{/if}}
7908
+ "
7909
+ `;
7910
+
7911
+ exports[`Assets Directory Snapshots > TypeScript assets > typescript/typescript/http/strands/base/package.json should match snapshot 1`] = `
7912
+ "{
7913
+ "name": "{{name}}",
7914
+ "version": "0.1.0",
7915
+ "description": "AgentCore Runtime Application using Strands TypeScript SDK",
7916
+ "private": true,
7917
+ "type": "module",
7918
+ "scripts": {
7919
+ "build": "tsc",
7920
+ "start": "node dist/main.js",
7921
+ "dev": "tsx watch main.ts"
7922
+ },
7923
+ "dependencies": {
7924
+ {{#if (eq modelProvider "Anthropic")}}
7925
+ "@anthropic-ai/sdk": "^0.71.2",
7926
+ {{/if}}
7927
+ {{#if (eq modelProvider "OpenAI")}}
7928
+ "openai": "^6.7.0",
7929
+ {{/if}}
7930
+ {{#if (eq modelProvider "Gemini")}}
7931
+ "@google/genai": "^1.40.0",
7932
+ {{/if}}
7933
+ "@modelcontextprotocol/sdk": "^1.25.2",
7934
+ "@opentelemetry/api": "^1.9.0",
7935
+ "@strands-agents/sdk": "^1.5.0",
7936
+ "bedrock-agentcore": "^0.3.0",
7937
+ "tsx": "^4.19.0",
7938
+ "zod": "^4.4.3"
7939
+ },
7940
+ "devDependencies": {
7941
+ "@types/node": "^22.0.0",
7942
+ "typescript": "^5.6.0"
7943
+ },
7944
+ "overrides": {
7945
+ "bedrock-agentcore": {
7946
+ "@strands-agents/sdk": "$@strands-agents/sdk"
7947
+ }
7948
+ }
7949
+ }
7950
+ "
7951
+ `;
7952
+
7953
+ exports[`Assets Directory Snapshots > TypeScript assets > typescript/typescript/http/strands/base/tsconfig.json should match snapshot 1`] = `
7954
+ "{
7955
+ "compilerOptions": {
7956
+ "target": "ES2022",
7957
+ "module": "NodeNext",
7958
+ "moduleResolution": "NodeNext",
7959
+ "strict": true,
7960
+ "esModuleInterop": true,
7961
+ "skipLibCheck": true,
7962
+ "forceConsistentCasingInFileNames": true,
7963
+ "resolveJsonModule": true,
7964
+ "declaration": false,
7965
+ "sourceMap": true,
7966
+ "outDir": "dist",
7967
+ "rootDir": ".",
7968
+ "types": ["node"]
7969
+ },
7970
+ "include": ["**/*.ts"],
7971
+ "exclude": ["node_modules", "dist"]
7972
+ }
7973
+ "
7974
+ `;
7975
+
7976
+ exports[`Assets Directory Snapshots > TypeScript assets > typescript/typescript/http/strands/capabilities/memory/memory.ts should match snapshot 1`] = `
7977
+ "import { randomUUID } from 'node:crypto';
7978
+ import { MemoryManager } from '@strands-agents/sdk';
7979
+ import { createAgentCoreMemoryStores } from 'bedrock-agentcore/experimental/memory/strands';
7980
+
7981
+ const MEMORY_ID = process.env.{{memoryProviders.[0].envVarName}};
7982
+
7983
+ const CUSTOM_ACTOR_ID_HEADER = 'x-amzn-bedrock-agentcore-runtime-custom-actor-id';
7984
+
7985
+ export function getActorId(payload: any, context: any): string {
7986
+ const raw =
7987
+ context?.headers?.[CUSTOM_ACTOR_ID_HEADER] ||
7988
+ payload?.userId ||
7989
+ context?.sessionId;
7990
+ return typeof raw === 'string' && raw.trim().length > 0 ? raw.trim() : randomUUID();
7991
+ }
7992
+
7993
+ const memoryManagerCache = new Map<string, MemoryManager>();
7994
+
7995
+ export function getOrCreateMemoryManager(sessionId: string, actorId: string): MemoryManager | null {
7996
+ if (!MEMORY_ID) return null;
7997
+
7998
+ const key = \`\${actorId}:\${sessionId}\`;
7999
+ let manager = memoryManagerCache.get(key);
8000
+ if (manager) return manager;
8001
+
8002
+ const stores = createAgentCoreMemoryStores({
8003
+ memoryId: MEMORY_ID,
8004
+ actorId,
8005
+ sessionId,
8006
+ namespaces: [
8007
+ {{#if (includes memoryProviders.[0].strategies "SEMANTIC")}}
8008
+ { namespace: '/users/{actorId}/facts' },
8009
+ {{/if}}
8010
+ {{#if (includes memoryProviders.[0].strategies "USER_PREFERENCE")}}
8011
+ { namespace: '/users/{actorId}/preferences' },
8012
+ {{/if}}
8013
+ {{#if (includes memoryProviders.[0].strategies "EPISODIC")}}
8014
+ { namespace: '/episodes/{actorId}/{sessionId}' },
8015
+ {{/if}}
8016
+ {{#if (includes memoryProviders.[0].strategies "SUMMARIZATION")}}
8017
+ { namespace: '/summaries/{actorId}/{sessionId}' },
8018
+ {{/if}}
8019
+ ],
8020
+ // readMode defaults to 'per-namespace' (one retrieve call per namespace).
8021
+ // Switch to 'subtree' to consolidate to a single hierarchical recall call.
8022
+ extraction: true,
8023
+ });
8024
+
8025
+ manager = new MemoryManager({ stores });
8026
+ memoryManagerCache.set(key, manager);
8027
+ return manager;
8028
+ }
8029
+ "
8030
+ `;
8031
+
8032
+ exports[`Assets Directory Snapshots > TypeScript assets > typescript/typescript/http/vercelai/base/README.md should match snapshot 1`] = `
8033
+ "This is a project generated by the AgentCore CLI!
8034
+
8035
+ # Layout
8036
+
8037
+ The generated application code lives at the agent root directory. At the root, there is a \`.gitignore\` file, an
8038
+ \`agentcore/\` folder which represents the configurations and state associated with this project. Other \`agentcore\`
8039
+ commands like \`deploy\`, \`dev\`, and \`invoke\` rely on the configuration stored here.
8040
+
8041
+ ## Agent Root
8042
+
8043
+ The main entrypoint to your app is defined in \`main.ts\`. Using the AgentCore SDK \`BedrockAgentCoreApp\`, this file
8044
+ defines an HTTP app that streams tokens using the Vercel AI SDK's \`streamText\` API.
8045
+
8046
+ \`model/load.ts\` instantiates your chosen model provider.
8047
+
8048
+ ## Environment Variables
8049
+
8050
+ | Variable | Required | Description |
8051
+ | --- | --- | --- |
8052
+ {{#if hasIdentity}}| \`{{identityProviders.[0].envVarName}}\` | Yes | {{modelProvider}} API key (local) or Identity provider name (deployed) |
8053
+ {{/if}}| \`LOCAL_DEV\` | No | Set to \`1\` to use \`.env.local\` instead of AgentCore Identity |
8054
+
8055
+ # Developing locally
8056
+
8057
+ If installation was successful, \`node_modules/\` is already populated with dependencies.
8058
+
8059
+ \`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\`).
8060
+
8061
+ In a new terminal, you can invoke that server with:
8062
+
8063
+ \`agentcore invoke --dev "What can you do"\`
8064
+
8065
+ # Deployment
8066
+
8067
+ After providing credentials, \`agentcore deploy\` will deploy your project into Amazon Bedrock AgentCore.
8068
+
8069
+ Use \`agentcore invoke\` to invoke your deployed agent.
8070
+ "
8071
+ `;
8072
+
8073
+ exports[`Assets Directory Snapshots > TypeScript assets > typescript/typescript/http/vercelai/base/gitignore.template should match snapshot 1`] = `
8074
+ "# Environment variables
8075
+ .env
8076
+ .env.*
8077
+
8078
+ # Node
8079
+ node_modules/
8080
+ dist/
8081
+ *.log
8082
+ npm-debug.log*
8083
+ yarn-debug.log*
8084
+ yarn-error.log*
8085
+
8086
+ # IDE
8087
+ .vscode/
8088
+ .idea/
8089
+ *.swp
8090
+ *.swo
8091
+ *~
8092
+
8093
+ # OS
8094
+ .DS_Store
8095
+ Thumbs.db
8096
+ "
8097
+ `;
8098
+
8099
+ exports[`Assets Directory Snapshots > TypeScript assets > typescript/typescript/http/vercelai/base/main.ts should match snapshot 1`] = `
8100
+ "import { BedrockAgentCoreApp } from 'bedrock-agentcore/runtime';
8101
+ import { streamText, type ModelMessage } from 'ai';
8102
+ import { loadModel } from './model/load.js';
8103
+
8104
+ const SYSTEM_PROMPT = \`You are a helpful assistant.\`;
8105
+
8106
+ const HISTORY_LIMIT = 128;
8107
+
8108
+ // Keeps one message history per sessionId so each session remembers its own
8109
+ // turns (best-effort; resets on cold start). A Map preserves insertion order,
8110
+ // so it doubles as an LRU bounded to 128 sessions — a local dev process serving
8111
+ // many sessions cannot leak history between them or grow without bound. On
8112
+ // AgentCore Runtime each microVM serves a single session, so this holds one
8113
+ // entry. For durable history, persist messages to an external store.
8114
+ const histories = new Map<string, ModelMessage[]>();
8115
+
8116
+ function getHistory(sessionId: string): ModelMessage[] {
8117
+ const existing = histories.get(sessionId);
8118
+ if (existing) {
8119
+ histories.delete(sessionId);
8120
+ histories.set(sessionId, existing);
8121
+ return existing;
8122
+ }
8123
+ if (histories.size >= HISTORY_LIMIT) {
8124
+ const oldest = histories.keys().next().value;
8125
+ if (oldest !== undefined) histories.delete(oldest);
8126
+ }
8127
+ const fresh: ModelMessage[] = [];
8128
+ histories.set(sessionId, fresh);
8129
+ return fresh;
8130
+ }
8131
+
8132
+ const app = new BedrockAgentCoreApp({
8133
+ invocationHandler: {
8134
+ async *process(payload: any, context: any) {
8135
+ const sessionId = context?.sessionId ?? 'default-session';
8136
+ const history = getHistory(sessionId);
8137
+ const userMessage: ModelMessage = { role: 'user', content: payload.prompt ?? '' };
8138
+
8139
+ const model = await loadModel();
8140
+ const result = streamText({
8141
+ model,
8142
+ system: SYSTEM_PROMPT,
8143
+ messages: [...history, userMessage],
8144
+ });
8145
+
8146
+ let assistant = '';
8147
+ for await (const chunk of result.textStream) {
8148
+ assistant += chunk;
8149
+ yield { data: chunk };
8150
+ }
8151
+
8152
+ // Commit the exchange to history only after a non-empty reply. On a failed
8153
+ // or empty stream the turn is dropped instead of leaving a dangling user
8154
+ // (or empty assistant) message — consecutive same-role or empty-content
8155
+ // messages would otherwise be rejected on the next turn for this session.
8156
+ if (assistant.length > 0) {
8157
+ history.push(userMessage, { role: 'assistant', content: assistant });
8158
+ }
8159
+ },
8160
+ },
8161
+ });
8162
+
8163
+ app.run({ port: parseInt(process.env.PORT ?? '8080') });
8164
+ "
8165
+ `;
8166
+
8167
+ exports[`Assets Directory Snapshots > TypeScript assets > typescript/typescript/http/vercelai/base/model/load.ts should match snapshot 1`] = `
8168
+ "{{#if (eq modelProvider "Bedrock")}}
8169
+ import { createAmazonBedrock } from '@ai-sdk/amazon-bedrock';
8170
+ import { fromNodeProviderChain } from '@aws-sdk/credential-providers';
8171
+
8172
+ const provider = fromNodeProviderChain();
8173
+
8174
+ const bedrock = createAmazonBedrock({
8175
+ region: process.env.AWS_REGION ?? 'us-east-1',
8176
+ credentialProvider: async () => {
8177
+ const creds = await provider();
8178
+ return {
8179
+ accessKeyId: creds.accessKeyId,
8180
+ secretAccessKey: creds.secretAccessKey,
8181
+ sessionToken: creds.sessionToken,
8182
+ };
8183
+ },
8184
+ });
8185
+
8186
+ export function loadModel() {
8187
+ return bedrock('us.anthropic.claude-sonnet-4-5-20250929-v1:0');
8188
+ }
8189
+ {{/if}}
8190
+ {{#if (eq modelProvider "Anthropic")}}
8191
+ import { createAnthropic } from '@ai-sdk/anthropic';
8192
+ import { withApiKey } from 'bedrock-agentcore/identity';
8193
+
8194
+ const IDENTITY_PROVIDER_NAME = '{{identityProviders.[0].name}}';
8195
+ const IDENTITY_ENV_VAR = '{{identityProviders.[0].envVarName}}';
8196
+
8197
+ async function getApiKey(): Promise<string> {
8198
+ if (process.env.LOCAL_DEV === '1') {
8199
+ const apiKey = process.env[IDENTITY_ENV_VAR] ?? process.env.ANTHROPIC_API_KEY;
8200
+ if (!apiKey) {
8201
+ throw new Error(\`\${IDENTITY_ENV_VAR} or ANTHROPIC_API_KEY not found. Add your key to agentcore/.env.local\`);
8202
+ }
8203
+ return apiKey;
8204
+ }
8205
+ return withApiKey({ providerName: IDENTITY_PROVIDER_NAME })(async (apiKey: string) => apiKey)();
8206
+ }
8207
+
8208
+ let _anthropic: ReturnType<typeof createAnthropic> | undefined;
8209
+
8210
+ async function getProvider() {
8211
+ if (!_anthropic) {
8212
+ const apiKey = await getApiKey();
8213
+ _anthropic = createAnthropic({ apiKey });
8214
+ }
8215
+ return _anthropic;
8216
+ }
8217
+
8218
+ export async function loadModel() {
8219
+ const anthropic = await getProvider();
8220
+ return anthropic('claude-sonnet-4-5-20250929');
8221
+ }
8222
+ {{/if}}
8223
+ {{#if (eq modelProvider "OpenAI")}}
8224
+ import { createOpenAI } from '@ai-sdk/openai';
8225
+ import { withApiKey } from 'bedrock-agentcore/identity';
8226
+
8227
+ const IDENTITY_PROVIDER_NAME = '{{identityProviders.[0].name}}';
8228
+ const IDENTITY_ENV_VAR = '{{identityProviders.[0].envVarName}}';
8229
+
8230
+ async function getApiKey(): Promise<string> {
8231
+ if (process.env.LOCAL_DEV === '1') {
8232
+ const apiKey = process.env[IDENTITY_ENV_VAR] ?? process.env.OPENAI_API_KEY;
8233
+ if (!apiKey) {
8234
+ throw new Error(\`\${IDENTITY_ENV_VAR} or OPENAI_API_KEY not found. Add your key to agentcore/.env.local\`);
8235
+ }
8236
+ return apiKey;
8237
+ }
8238
+ return withApiKey({ providerName: IDENTITY_PROVIDER_NAME })(async (apiKey: string) => apiKey)();
8239
+ }
8240
+
8241
+ let _openai: ReturnType<typeof createOpenAI> | undefined;
8242
+
8243
+ async function getProvider() {
8244
+ if (!_openai) {
8245
+ const apiKey = await getApiKey();
8246
+ _openai = createOpenAI({ apiKey });
8247
+ }
8248
+ return _openai;
8249
+ }
8250
+
8251
+ export async function loadModel() {
8252
+ const openai = await getProvider();
8253
+ return openai('gpt-4.1');
8254
+ }
8255
+ {{/if}}
8256
+ {{#if (eq modelProvider "Gemini")}}
8257
+ import { createGoogleGenerativeAI } from '@ai-sdk/google';
8258
+ import { withApiKey } from 'bedrock-agentcore/identity';
8259
+
8260
+ const IDENTITY_PROVIDER_NAME = '{{identityProviders.[0].name}}';
8261
+ const IDENTITY_ENV_VAR = '{{identityProviders.[0].envVarName}}';
8262
+
8263
+ async function getApiKey(): Promise<string> {
8264
+ if (process.env.LOCAL_DEV === '1') {
8265
+ const apiKey = process.env[IDENTITY_ENV_VAR] ?? process.env.GEMINI_API_KEY;
8266
+ if (!apiKey) {
8267
+ throw new Error(\`\${IDENTITY_ENV_VAR} or GEMINI_API_KEY not found. Add your key to agentcore/.env.local\`);
8268
+ }
8269
+ return apiKey;
8270
+ }
8271
+ return withApiKey({ providerName: IDENTITY_PROVIDER_NAME })(async (apiKey: string) => apiKey)();
8272
+ }
8273
+
8274
+ let _google: ReturnType<typeof createGoogleGenerativeAI> | undefined;
8275
+
8276
+ async function getProvider() {
8277
+ if (!_google) {
8278
+ const apiKey = await getApiKey();
8279
+ _google = createGoogleGenerativeAI({ apiKey });
8280
+ }
8281
+ return _google;
8282
+ }
8283
+
8284
+ export async function loadModel() {
8285
+ const google = await getProvider();
8286
+ return google('gemini-2.5-flash');
8287
+ }
8288
+ {{/if}}
8289
+ "
8290
+ `;
8291
+
8292
+ exports[`Assets Directory Snapshots > TypeScript assets > typescript/typescript/http/vercelai/base/package.json should match snapshot 1`] = `
8293
+ "{
8294
+ "name": "{{name}}",
8295
+ "version": "0.1.0",
8296
+ "description": "AgentCore Runtime Application using Vercel AI SDK",
8297
+ "private": true,
8298
+ "type": "module",
8299
+ "scripts": {
8300
+ "build": "tsc",
8301
+ "start": "node dist/main.js",
8302
+ "dev": "tsx watch main.ts"
8303
+ },
8304
+ "dependencies": {
8305
+ "ai": "^6.0.0",
8306
+ {{#if (eq modelProvider "Bedrock")}}
8307
+ "@ai-sdk/amazon-bedrock": "^4.0.0",
8308
+ "@aws-sdk/credential-providers": "^3.0.0",
8309
+ {{/if}}
8310
+ {{#if (eq modelProvider "Anthropic")}}
8311
+ "@ai-sdk/anthropic": "^3.0.0",
8312
+ {{/if}}
8313
+ {{#if (eq modelProvider "OpenAI")}}
8314
+ "@ai-sdk/openai": "^3.0.0",
8315
+ {{/if}}
8316
+ {{#if (eq modelProvider "Gemini")}}
8317
+ "@ai-sdk/google": "^3.0.0",
8318
+ {{/if}}
8319
+ "bedrock-agentcore": "^0.2.4",
8320
+ "tsx": "^4.19.0",
8321
+ "zod": "^3.24.0"
8322
+ },
8323
+ "devDependencies": {
8324
+ "@types/node": "^22.0.0",
8325
+ "typescript": "^5.6.0"
8326
+ }
8327
+ }
8328
+ "
8329
+ `;
8330
+
8331
+ exports[`Assets Directory Snapshots > TypeScript assets > typescript/typescript/http/vercelai/base/tsconfig.json should match snapshot 1`] = `
8332
+ "{
8333
+ "compilerOptions": {
8334
+ "target": "ES2022",
8335
+ "module": "NodeNext",
8336
+ "moduleResolution": "NodeNext",
8337
+ "strict": true,
8338
+ "esModuleInterop": true,
8339
+ "skipLibCheck": true,
8340
+ "forceConsistentCasingInFileNames": true,
8341
+ "resolveJsonModule": true,
8342
+ "declaration": false,
8343
+ "sourceMap": true,
8344
+ "outDir": "dist",
8345
+ "rootDir": ".",
8346
+ "types": ["node"]
8347
+ },
8348
+ "include": ["**/*.ts"],
8349
+ "exclude": ["node_modules", "dist"]
8350
+ }
8351
+ "
8352
+ `;