@aws/agentcore 1.0.0-preview.12 → 1.0.0-preview.14

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 (112) hide show
  1. package/README.md +31 -17
  2. package/dist/agent-inspector/index.css +1 -1
  3. package/dist/agent-inspector/index.js +90 -85
  4. package/dist/assets/__tests__/__snapshots__/assets.snapshot.test.ts.snap +1373 -113
  5. package/dist/assets/agents/AGENTS.md +14 -0
  6. package/dist/assets/cdk/bin/cdk.ts +90 -24
  7. package/dist/assets/cdk/lib/cdk-stack.ts +178 -12
  8. package/dist/assets/cdk/test/cdk.test.ts +3 -0
  9. package/dist/assets/container/python/Dockerfile +4 -0
  10. package/dist/assets/python/a2a/langchain_langgraph/base/pyproject.toml +1 -1
  11. package/dist/assets/python/a2a/strands/capabilities/memory/session.py +4 -1
  12. package/dist/assets/python/agui/googleadk/base/pyproject.toml +1 -1
  13. package/dist/assets/python/agui/langchain_langgraph/base/pyproject.toml +1 -1
  14. package/dist/assets/python/agui/strands/capabilities/memory/session.py +4 -1
  15. package/dist/assets/python/http/autogen/base/pyproject.toml +1 -1
  16. package/dist/assets/python/http/googleadk/base/mcp_client/client.py +7 -7
  17. package/dist/assets/python/http/googleadk/base/pyproject.toml +1 -1
  18. package/dist/assets/python/http/langchain_langgraph/base/mcp_client/client.py +10 -8
  19. package/dist/assets/python/http/langchain_langgraph/base/pyproject.toml +2 -2
  20. package/dist/assets/python/http/openaiagents/base/main.py +19 -9
  21. package/dist/assets/python/http/openaiagents/base/mcp_client/client.py +4 -4
  22. package/dist/assets/python/http/openaiagents/base/pyproject.toml +2 -2
  23. package/dist/assets/python/http/strands/base/main.py +475 -27
  24. package/dist/assets/python/http/strands/base/mcp_client/client.py +45 -5
  25. package/dist/assets/python/http/strands/base/model/load.py +3 -3
  26. package/dist/assets/python/http/strands/base/pyproject.toml +5 -2
  27. package/dist/assets/python/http/strands/base/skills/fetcher.py +279 -0
  28. package/dist/assets/python/http/strands/capabilities/execution-limits/hooks/execution_limits.py +54 -0
  29. package/dist/assets/python/http/strands/capabilities/memory/session.py +4 -1
  30. package/dist/assets/python/http/strands/capabilities/payments/__init__.py +4 -0
  31. package/dist/assets/python/http/strands/capabilities/payments/payments.py +142 -0
  32. package/dist/cli/index.mjs +1235 -983
  33. package/dist/lib/errors/types.d.ts +18 -0
  34. package/dist/lib/errors/types.d.ts.map +1 -1
  35. package/dist/lib/errors/types.js +28 -1
  36. package/dist/lib/errors/types.js.map +1 -1
  37. package/dist/lib/packaging/helpers.d.ts.map +1 -1
  38. package/dist/lib/packaging/helpers.js +50 -10
  39. package/dist/lib/packaging/helpers.js.map +1 -1
  40. package/dist/lib/schemas/io/config-io.d.ts.map +1 -1
  41. package/dist/lib/schemas/io/config-io.js +0 -2
  42. package/dist/lib/schemas/io/config-io.js.map +1 -1
  43. package/dist/lib/utils/env.d.ts +4 -0
  44. package/dist/lib/utils/env.d.ts.map +1 -1
  45. package/dist/lib/utils/env.js +18 -0
  46. package/dist/lib/utils/env.js.map +1 -1
  47. package/dist/lib/utils/index.d.ts +1 -1
  48. package/dist/lib/utils/index.d.ts.map +1 -1
  49. package/dist/lib/utils/index.js +2 -1
  50. package/dist/lib/utils/index.js.map +1 -1
  51. package/dist/schema/constants.d.ts +20 -0
  52. package/dist/schema/constants.d.ts.map +1 -1
  53. package/dist/schema/constants.js +25 -1
  54. package/dist/schema/constants.js.map +1 -1
  55. package/dist/schema/schemas/agent-env.d.ts +35 -0
  56. package/dist/schema/schemas/agent-env.d.ts.map +1 -1
  57. package/dist/schema/schemas/agentcore-project.d.ts +239 -25
  58. package/dist/schema/schemas/agentcore-project.d.ts.map +1 -1
  59. package/dist/schema/schemas/agentcore-project.js +140 -34
  60. package/dist/schema/schemas/agentcore-project.js.map +1 -1
  61. package/dist/schema/schemas/auth.d.ts +175 -0
  62. package/dist/schema/schemas/auth.d.ts.map +1 -1
  63. package/dist/schema/schemas/auth.js +126 -3
  64. package/dist/schema/schemas/auth.js.map +1 -1
  65. package/dist/schema/schemas/deployed-state.d.ts +237 -46
  66. package/dist/schema/schemas/deployed-state.d.ts.map +1 -1
  67. package/dist/schema/schemas/deployed-state.js +62 -17
  68. package/dist/schema/schemas/deployed-state.js.map +1 -1
  69. package/dist/schema/schemas/mcp-defs.d.ts +2 -1
  70. package/dist/schema/schemas/mcp-defs.d.ts.map +1 -1
  71. package/dist/schema/schemas/mcp-defs.js +3 -2
  72. package/dist/schema/schemas/mcp-defs.js.map +1 -1
  73. package/dist/schema/schemas/mcp.d.ts +186 -0
  74. package/dist/schema/schemas/mcp.d.ts.map +1 -1
  75. package/dist/schema/schemas/mcp.js +479 -4
  76. package/dist/schema/schemas/mcp.js.map +1 -1
  77. package/dist/schema/schemas/primitives/ab-test.d.ts +0 -6
  78. package/dist/schema/schemas/primitives/ab-test.d.ts.map +1 -1
  79. package/dist/schema/schemas/primitives/ab-test.js +0 -2
  80. package/dist/schema/schemas/primitives/ab-test.js.map +1 -1
  81. package/dist/schema/schemas/primitives/harness.d.ts +242 -11
  82. package/dist/schema/schemas/primitives/harness.d.ts.map +1 -1
  83. package/dist/schema/schemas/primitives/harness.js +309 -21
  84. package/dist/schema/schemas/primitives/harness.js.map +1 -1
  85. package/dist/schema/schemas/primitives/index.d.ts +8 -8
  86. package/dist/schema/schemas/primitives/index.d.ts.map +1 -1
  87. package/dist/schema/schemas/primitives/index.js +18 -5
  88. package/dist/schema/schemas/primitives/index.js.map +1 -1
  89. package/dist/schema/schemas/primitives/knowledge-base.d.ts +104 -0
  90. package/dist/schema/schemas/primitives/knowledge-base.d.ts.map +1 -0
  91. package/dist/schema/schemas/primitives/knowledge-base.js +99 -0
  92. package/dist/schema/schemas/primitives/knowledge-base.js.map +1 -0
  93. package/dist/schema/schemas/primitives/online-eval-config.d.ts +20 -2
  94. package/dist/schema/schemas/primitives/online-eval-config.d.ts.map +1 -1
  95. package/dist/schema/schemas/primitives/online-eval-config.js +43 -5
  96. package/dist/schema/schemas/primitives/online-eval-config.js.map +1 -1
  97. package/dist/schema/schemas/primitives/payment.d.ts +54 -0
  98. package/dist/schema/schemas/primitives/payment.d.ts.map +1 -0
  99. package/dist/schema/schemas/primitives/payment.js +79 -0
  100. package/dist/schema/schemas/primitives/payment.js.map +1 -0
  101. package/dist/schema/schemas/primitives/policy.d.ts +26 -0
  102. package/dist/schema/schemas/primitives/policy.d.ts.map +1 -1
  103. package/dist/schema/schemas/primitives/policy.js +5 -1
  104. package/dist/schema/schemas/primitives/policy.js.map +1 -1
  105. package/npm-shrinkwrap.json +941 -670
  106. package/package.json +11 -8
  107. package/scripts/bundle.mjs +51 -0
  108. package/scripts/run-e2e-local.sh +48 -10
  109. package/dist/schema/schemas/primitives/http-gateway.d.ts +0 -21
  110. package/dist/schema/schemas/primitives/http-gateway.d.ts.map +0 -1
  111. package/dist/schema/schemas/primitives/http-gateway.js +0 -34
  112. package/dist/schema/schemas/primitives/http-gateway.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,38 +101,61 @@ 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.
104
+ // Read harness configs: the full validated spec drives the CFN resource; the
105
+ // role-scoped fields drive the IAM role + container build.
103
106
  const projectRoot = path.resolve(configRoot, '..');
104
- const harnessConfigs: {
105
- name: string;
106
- executionRoleArn?: string;
107
- memoryName?: string;
108
- containerUri?: string;
109
- hasDockerfile?: boolean;
110
- dockerfile?: string;
111
- codeLocation?: string;
112
- tools?: { type: string; name: string }[];
113
- apiKeyArn?: string;
114
- efsAccessPoints?: { accessPointArn: string; mountPath: string }[];
115
- s3AccessPoints?: { accessPointArn: string; mountPath: string }[];
116
- }[] = [];
117
- for (const entry of specAny.harnesses ?? []) {
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
+ // Harness is preview-gated. The CLI bundle bakes the preview flag at build time and
128
+ // forwards it to this child process via AGENTCORE_PREVIEW (see toolkit-lib/wrapper.ts).
129
+ // This app is built separately and cannot see that build-time define, so it gates on the
130
+ // env var. Absent/anything-but-'1' defaults to off so a stale harnesses[] entry in a
131
+ // non-preview build never synthesizes an AWS::BedrockAgentCore::Harness resource.
132
+ const previewEnabled = process.env.AGENTCORE_PREVIEW === '1';
133
+
134
+ const harnessConfigs: HarnessConfig[] = [];
135
+ for (const entry of previewEnabled ? (specAny.harnesses ?? []) : []) {
118
136
  const harnessDir = path.resolve(projectRoot, entry.path);
119
137
  const harnessPath = path.resolve(harnessDir, 'harness.json');
120
138
  try {
121
- const harnessSpec = JSON.parse(fs.readFileSync(harnessPath, 'utf-8'));
139
+ const harnessSpec = HarnessSpecSchema.parse(JSON.parse(fs.readFileSync(harnessPath, 'utf-8')));
122
140
  harnessConfigs.push({
123
141
  name: entry.name,
124
142
  executionRoleArn: harnessSpec.executionRoleArn,
125
- memoryName: harnessSpec.memory?.name,
143
+ // Only an \`existing\` memory ref carries a name to wire IAM against; managed memory is
144
+ // owned by the harness (no sibling) and disabled has none — both resolve to undefined.
145
+ memoryName: harnessSpec.memory?.mode === 'existing' ? harnessSpec.memory.name : undefined,
126
146
  containerUri: harnessSpec.containerUri,
127
147
  hasDockerfile: !!harnessSpec.dockerfile,
128
148
  dockerfile: harnessSpec.dockerfile,
129
149
  codeLocation: harnessSpec.dockerfile ? harnessDir : undefined,
130
150
  tools: harnessSpec.tools,
151
+ skills: harnessSpec.skills,
131
152
  apiKeyArn: harnessSpec.model?.apiKeyArn,
132
153
  efsAccessPoints: harnessSpec.efsAccessPoints,
133
154
  s3AccessPoints: harnessSpec.s3AccessPoints,
155
+ apiFormat: harnessSpec.model?.apiFormat,
156
+ // Full spec + dir drive the AWS::BedrockAgentCore::Harness CFN resource.
157
+ spec: harnessSpec,
158
+ harnessDir,
134
159
  });
135
160
  } catch (err) {
136
161
  throw new Error(
@@ -154,11 +179,52 @@ async function main() {
154
179
  | Record<string, { credentialProviderArn: string; clientSecretArn?: string }>
155
180
  | undefined;
156
181
 
182
+ // Payment credential provider ARNs live in the same credentials map as identity credentials
183
+ const paymentCredentials = credentials;
184
+
185
+ const paymentSpec = specAny.payments?.length
186
+ ? specAny.payments.map(
187
+ (p: {
188
+ name: string;
189
+ description?: string;
190
+ authorizerType: 'AWS_IAM' | 'CUSTOM_JWT';
191
+ authorizerConfiguration?: unknown;
192
+ autoPayment?: boolean;
193
+ paymentToolAllowlist?: string[];
194
+ networkPreferences?: string[];
195
+ connectors: { name: string; provider?: string; credentialName: string }[];
196
+ }) => ({
197
+ name: p.name,
198
+ description: p.description,
199
+ authorizerType: p.authorizerType,
200
+ authorizerConfiguration: p.authorizerConfiguration,
201
+ autoPayment: p.autoPayment,
202
+ paymentToolAllowlist: p.paymentToolAllowlist,
203
+ networkPreferences: p.networkPreferences,
204
+ connectors: p.connectors.map(c => {
205
+ const credentialProviderArn = paymentCredentials?.[c.credentialName]?.credentialProviderArn;
206
+ if (!credentialProviderArn) {
207
+ // Fail fast with an actionable message rather than passing an empty
208
+ // ARN that fails opaquely server-side at CreatePaymentConnector.
209
+ throw new Error(
210
+ \`Payment connector "\${c.name}" on manager "\${p.name}" references credential \` +
211
+ \`"\${c.credentialName}", but no deployed credential provider was found for it. \` +
212
+ \`Run \\\`agentcore deploy\\\` so the credential provider is created first.\`
213
+ );
214
+ }
215
+ return { name: c.name, provider: c.provider, credentialProviderArn };
216
+ }),
217
+ })
218
+ )
219
+ : undefined;
220
+
157
221
  new AgentCoreStack(app, stackName, {
158
222
  spec,
159
223
  mcpSpec,
160
224
  credentials,
225
+ connectorParametersByFile,
161
226
  harnesses: harnessConfigs.length > 0 ? harnessConfigs : undefined,
227
+ paymentSpec,
162
228
  env,
163
229
  description: \`AgentCore stack for \${spec.name} deployed to \${target.name} (\${target.region})\`,
164
230
  tags: {
@@ -173,7 +239,7 @@ async function main() {
173
239
 
174
240
  main().catch((error: unknown) => {
175
241
  console.error('AgentCore CDK synthesis failed:', error instanceof Error ? error.message : error);
176
- process.exitCode = 1;
242
+ process.exit(1);
177
243
  });
178
244
  "
179
245
  `;
@@ -300,24 +366,39 @@ exports[`Assets Directory Snapshots > CDK assets > cdk/cdk/lib/cdk-stack.ts shou
300
366
  "import {
301
367
  AgentCoreApplication,
302
368
  AgentCoreMcp,
369
+ AgentCorePaymentManager,
370
+ AgentCorePaymentConnector,
303
371
  type AgentCoreProjectSpec,
304
372
  type AgentCoreMcpSpec,
373
+ type CustomJWTAuthorizerConfig,
374
+ type HarnessDeploymentConfig,
305
375
  } from '@aws/agentcore-cdk';
306
376
  import { CfnOutput, Stack, type StackProps } from 'aws-cdk-lib';
377
+ import * as iam from 'aws-cdk-lib/aws-iam';
307
378
  import { Construct } from 'constructs';
308
379
 
309
- export interface HarnessConfig {
380
+ /**
381
+ * Harness deployment config: role-scoped fields (for IAM role + container build)
382
+ * plus the full validated spec + its config directory so the L3 construct can
383
+ * synthesize the AWS::BedrockAgentCore::Harness resource.
384
+ */
385
+ export type HarnessConfig = HarnessDeploymentConfig;
386
+
387
+ export interface PaymentConnectorSpec {
310
388
  name: string;
311
- executionRoleArn?: string;
312
- memoryName?: string;
313
- containerUri?: string;
314
- hasDockerfile?: boolean;
315
- dockerfile?: string;
316
- codeLocation?: string;
317
- tools?: { type: string; name: string }[];
318
- apiKeyArn?: string;
319
- efsAccessPoints?: { accessPointArn: string; mountPath: string }[];
320
- s3AccessPoints?: { accessPointArn: string; mountPath: string }[];
389
+ provider: 'CoinbaseCDP' | 'StripePrivy';
390
+ credentialProviderArn: string;
391
+ }
392
+
393
+ export interface PaymentSpec {
394
+ name: string;
395
+ description?: string;
396
+ authorizerType: 'AWS_IAM' | 'CUSTOM_JWT';
397
+ authorizerConfiguration?: { customJWTAuthorizer: CustomJWTAuthorizerConfig };
398
+ autoPayment?: boolean;
399
+ paymentToolAllowlist?: string[];
400
+ networkPreferences?: string[];
401
+ connectors: PaymentConnectorSpec[];
321
402
  }
322
403
 
323
404
  export interface AgentCoreStackProps extends StackProps {
@@ -337,6 +418,35 @@ export interface AgentCoreStackProps extends StackProps {
337
418
  * Harness role configurations.
338
419
  */
339
420
  harnesses?: HarnessConfig[];
421
+ /**
422
+ * Parsed connectorParameters for non-S3 KB data sources, keyed by
423
+ * connectorConfigFile path. Forwarded to AgentCoreApplication.
424
+ */
425
+ connectorParametersByFile?: Record<string, Record<string, unknown>>;
426
+ /**
427
+ * Payment specifications with resolved credential provider ARNs.
428
+ */
429
+ paymentSpec?: PaymentSpec[];
430
+ }
431
+
432
+ function toCdkId(name: string): string {
433
+ return name.replace(/_/g, '');
434
+ }
435
+
436
+ /**
437
+ * Decide whether a deployed runtime should receive payment env vars + IAM grants.
438
+ * Payments today only ships a runtime shim for Python HTTP runtimes; injecting
439
+ * AGENTCORE_PAYMENT_* env vars into TypeScript / MCP / A2A / AGUI runtimes
440
+ * would surface env vars they cannot consume and would dilute least-privilege
441
+ * IAM grants for runtimes that never call ProcessPayment.
442
+ */
443
+ function isPaymentEligibleAgent(agent: { entrypoint?: string; protocol?: string }): boolean {
444
+ if (agent.protocol && agent.protocol !== 'HTTP') {
445
+ return false;
446
+ }
447
+ const entrypoint = typeof agent.entrypoint === 'string' ? agent.entrypoint : '';
448
+ const entrypointFile = entrypoint.split(':')[0] ?? '';
449
+ return entrypointFile.endsWith('.py');
340
450
  }
341
451
 
342
452
  /**
@@ -352,7 +462,7 @@ export class AgentCoreStack extends Stack {
352
462
  constructor(scope: Construct, id: string, props: AgentCoreStackProps) {
353
463
  super(scope, id, props);
354
464
 
355
- const { spec, mcpSpec, credentials, harnesses } = props;
465
+ const { spec, mcpSpec, credentials, harnesses, connectorParametersByFile, paymentSpec } = props;
356
466
 
357
467
  // Create AgentCoreApplication with all agents and harness roles
358
468
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
@@ -360,6 +470,12 @@ export class AgentCoreStack extends Stack {
360
470
  if (harnesses?.length) {
361
471
  appProps.harnesses = harnesses;
362
472
  }
473
+ if (connectorParametersByFile && Object.keys(connectorParametersByFile).length > 0) {
474
+ appProps.connectorParametersByFile = connectorParametersByFile;
475
+ }
476
+ if (credentials) {
477
+ appProps.credentials = credentials;
478
+ }
363
479
  this.application = new AgentCoreApplication(this, 'Application', appProps as any);
364
480
 
365
481
  // Create AgentCoreMcp if there are gateways configured
@@ -373,6 +489,122 @@ export class AgentCoreStack extends Stack {
373
489
  });
374
490
  }
375
491
 
492
+ // Create payment infrastructure via CFN constructs
493
+ if (paymentSpec && paymentSpec.length > 0) {
494
+ for (const payment of paymentSpec) {
495
+ const mgrId = toCdkId(payment.name);
496
+ const manager = new AgentCorePaymentManager(this, \`Payment\${mgrId}\`, {
497
+ projectName: spec.name,
498
+ name: payment.name,
499
+ authorizerType: payment.authorizerType,
500
+ description: payment.description,
501
+ authorizerConfiguration: payment.authorizerConfiguration,
502
+ tags: spec.tags,
503
+ });
504
+
505
+ const prefix = \`AGENTCORE_PAYMENT_\${payment.name.toUpperCase().replace(/-/g, '_')}\`;
506
+
507
+ // Wire env vars from construct output tokens into eligible agent environments only.
508
+ // See isPaymentEligibleAgent — non-Python or non-HTTP runtimes have no shim that
509
+ // can consume these env vars, and giving them sts:AssumeRole on the
510
+ // ProcessPaymentRole would broaden the privilege surface unnecessarily.
511
+ for (const env of this.application.environments.values()) {
512
+ if (!isPaymentEligibleAgent(env.agent)) {
513
+ continue;
514
+ }
515
+ env.runtime.addEnvironmentVariable(\`\${prefix}_MANAGER_ARN\`, manager.paymentManagerArn);
516
+ env.runtime.addEnvironmentVariable(\`\${prefix}_PROCESS_PAYMENT_ROLE_ARN\`, manager.processPaymentRoleArn);
517
+
518
+ // Grant runtime execution role permission to assume the ProcessPaymentRole.
519
+ // The ProcessPaymentRole's trust policy allows AccountRootPrincipal, but the
520
+ // caller still needs sts:AssumeRole on its own role to perform the assumption.
521
+ env.runtime.role.addToPrincipalPolicy(
522
+ new iam.PolicyStatement({
523
+ actions: ['sts:AssumeRole'],
524
+ resources: [manager.processPaymentRoleArn],
525
+ })
526
+ );
527
+
528
+ // Grant payment data-plane actions directly to the runtime role.
529
+ //
530
+ // NOTE: This deviates from the canonical role model in the AgentCore Payments
531
+ // beta guide, which assigns Get/List/Create instrument+session actions to a
532
+ // separate ManagementRole and limits the agent's role to ProcessPayment only.
533
+ // The current SDK plugin (AgentCorePaymentsPlugin.generate_payment_header)
534
+ // calls GetPaymentInstrument internally during the 402 auto-pay path, so the
535
+ // runtime role needs read access. CreatePaymentSession is included so
536
+ // \`agentcore invoke --auto-session\` works without a separate ManagementRole
537
+ // call. Tighten this if the SDK is updated to accept pre-fetched instrument
538
+ // details and split create-session into a backend-only flow.
539
+ env.runtime.role.addToPrincipalPolicy(
540
+ new iam.PolicyStatement({
541
+ actions: [
542
+ 'bedrock-agentcore:GetPaymentInstrument',
543
+ 'bedrock-agentcore:ListPaymentInstruments',
544
+ 'bedrock-agentcore:GetPaymentInstrumentBalance',
545
+ 'bedrock-agentcore:GetPaymentSession',
546
+ 'bedrock-agentcore:ListPaymentSessions',
547
+ 'bedrock-agentcore:CreatePaymentSession',
548
+ 'bedrock-agentcore:ProcessPayment',
549
+ ],
550
+ resources: [manager.paymentManagerArn, \`\${manager.paymentManagerArn}/*\`],
551
+ })
552
+ );
553
+
554
+ if (payment.autoPayment !== undefined) {
555
+ env.runtime.addEnvironmentVariable(\`\${prefix}_AUTO_PAYMENT\`, String(payment.autoPayment));
556
+ }
557
+ if (payment.paymentToolAllowlist) {
558
+ env.runtime.addEnvironmentVariable(\`\${prefix}_TOOL_ALLOWLIST\`, payment.paymentToolAllowlist.join(','));
559
+ }
560
+ if (payment.networkPreferences) {
561
+ env.runtime.addEnvironmentVariable(\`\${prefix}_NETWORK_PREFERENCES\`, payment.networkPreferences.join(','));
562
+ }
563
+ if (payment.authorizerType === 'CUSTOM_JWT') {
564
+ env.runtime.addEnvironmentVariable(\`\${prefix}_AUTH_MODE\`, 'bearer');
565
+ }
566
+ }
567
+
568
+ // Create connectors for this manager
569
+ for (const connector of payment.connectors) {
570
+ const connId = toCdkId(connector.name);
571
+ const conn = new AgentCorePaymentConnector(this, \`Payment\${mgrId}\${connId}\`, {
572
+ projectName: spec.name,
573
+ paymentManager: manager,
574
+ connectorName: connector.name,
575
+ connectorType: connector.provider,
576
+ credentialProviderArn: connector.credentialProviderArn,
577
+ });
578
+
579
+ // Wire first connector's ID as env var (eligible agents only)
580
+ if (connector === payment.connectors[0]) {
581
+ for (const env of this.application.environments.values()) {
582
+ if (!isPaymentEligibleAgent(env.agent)) continue;
583
+ env.runtime.addEnvironmentVariable(\`\${prefix}_CONNECTOR_ID\`, conn.paymentConnectorId);
584
+ }
585
+ }
586
+
587
+ new CfnOutput(this, \`Payment\${mgrId}\${connId}ConnectorId\`, {
588
+ value: conn.paymentConnectorId,
589
+ });
590
+ }
591
+
592
+ // CFN Outputs for post-deploy state parsing
593
+ new CfnOutput(this, \`Payment\${mgrId}ManagerArn\`, {
594
+ value: manager.paymentManagerArn,
595
+ });
596
+ new CfnOutput(this, \`Payment\${mgrId}ManagerId\`, {
597
+ value: manager.paymentManagerId,
598
+ });
599
+ new CfnOutput(this, \`Payment\${mgrId}ProcessPaymentRoleArn\`, {
600
+ value: manager.processPaymentRoleArn,
601
+ });
602
+ new CfnOutput(this, \`Payment\${mgrId}ResourceRetrievalRoleArn\`, {
603
+ value: manager.resourceRetrievalRoleArn,
604
+ });
605
+ }
606
+ }
607
+
376
608
  // Stack-level output
377
609
  new CfnOutput(this, 'StackNameOutput', {
378
610
  description: 'Name of the CloudFormation Stack',
@@ -444,11 +676,14 @@ test('AgentCoreStack synthesizes with empty spec', () => {
444
676
  credentials: [],
445
677
  evaluators: [],
446
678
  onlineEvalConfigs: [],
679
+ configBundles: [],
447
680
  policyEngines: [],
681
+ payments: [],
448
682
  agentCoreGateways: [],
449
683
  mcpRuntimeTools: [],
450
684
  unassignedTargets: [],
451
685
  datasets: [],
686
+ knowledgeBases: [],
452
687
  },
453
688
  });
454
689
  const template = Template.fromStack(stack);
@@ -603,8 +838,12 @@ exports[`Assets Directory Snapshots > File listing > should match the expected f
603
838
  "python/http/strands/base/model/__init__.py",
604
839
  "python/http/strands/base/model/load.py",
605
840
  "python/http/strands/base/pyproject.toml",
841
+ "python/http/strands/base/skills/fetcher.py",
842
+ "python/http/strands/capabilities/execution-limits/hooks/execution_limits.py",
606
843
  "python/http/strands/capabilities/memory/__init__.py",
607
844
  "python/http/strands/capabilities/memory/session.py",
845
+ "python/http/strands/capabilities/payments/__init__.py",
846
+ "python/http/strands/capabilities/payments/payments.py",
608
847
  "python/mcp/standalone/base/README.md",
609
848
  "python/mcp/standalone/base/gitignore.template",
610
849
  "python/mcp/standalone/base/main.py",
@@ -1629,7 +1868,7 @@ dependencies = [
1629
1868
  {{/if}}{{#if (eq modelProvider "OpenAI")}}"langchain-openai >= 0.2.0",
1630
1869
  {{/if}}"aws-opentelemetry-distro",
1631
1870
  "opentelemetry-instrumentation-langchain >= 0.59.0",
1632
- "bedrock-agentcore[a2a] >= 1.0.3",
1871
+ "bedrock-agentcore[a2a] >= 1.8.0",
1633
1872
  "botocore[crt] >= 1.35.0",
1634
1873
  "langgraph >= 0.2.0",
1635
1874
  ]
@@ -2018,8 +2257,11 @@ def get_memory_session_manager(session_id: Optional[str], actor_id: str) -> Opti
2018
2257
  {{#if (includes memoryProviders.[0].strategies "USER_PREFERENCE")}}
2019
2258
  f"/users/{actor_id}/preferences": RetrievalConfig(top_k=3, relevance_score=0.5),
2020
2259
  {{/if}}
2260
+ {{#if (includes memoryProviders.[0].strategies "EPISODIC")}}
2261
+ f"/episodes/{actor_id}/{session_id}": RetrievalConfig(top_k=5, relevance_score=0.5),
2262
+ {{/if}}
2021
2263
  {{#if (includes memoryProviders.[0].strategies "SUMMARIZATION")}}
2022
- f"/summaries/{actor_id}": RetrievalConfig(top_k=3, relevance_score=0.5),
2264
+ f"/summaries/{actor_id}/{session_id}": RetrievalConfig(top_k=3, relevance_score=0.5),
2023
2265
  {{/if}}
2024
2266
  }
2025
2267
  {{/if}}
@@ -2276,7 +2518,7 @@ requires-python = ">=3.10"
2276
2518
  dependencies = [
2277
2519
  "ag-ui-adk >= 0.6.0",
2278
2520
  "ag-ui-protocol >= 0.1.10",
2279
- "bedrock-agentcore >= 1.0.3",
2521
+ "bedrock-agentcore >= 1.8.0",
2280
2522
  "fastapi >= 0.115.12",
2281
2523
  "google-adk >= 1.16.0, < 2.0.0",
2282
2524
  "google-genai >= 1.0.0, < 2.0.0",
@@ -2648,7 +2890,7 @@ dependencies = [
2648
2890
  {{/if}}{{#if (eq modelProvider "OpenAI")}}"langchain-openai >= 0.2.0",
2649
2891
  {{/if}}"aws-opentelemetry-distro",
2650
2892
  "opentelemetry-instrumentation-langchain >= 0.59.0",
2651
- "bedrock-agentcore >= 1.0.3",
2893
+ "bedrock-agentcore >= 1.8.0",
2652
2894
  "botocore[crt] >= 1.35.0",
2653
2895
  "langgraph >= 0.3.25",
2654
2896
  "langchain >= 0.3.0",
@@ -3037,8 +3279,11 @@ def get_memory_session_manager(session_id: Optional[str], actor_id: str) -> Opti
3037
3279
  {{#if (includes memoryProviders.[0].strategies "USER_PREFERENCE")}}
3038
3280
  f"/users/{actor_id}/preferences": RetrievalConfig(top_k=3, relevance_score=0.5),
3039
3281
  {{/if}}
3282
+ {{#if (includes memoryProviders.[0].strategies "EPISODIC")}}
3283
+ f"/episodes/{actor_id}/{session_id}": RetrievalConfig(top_k=5, relevance_score=0.5),
3284
+ {{/if}}
3040
3285
  {{#if (includes memoryProviders.[0].strategies "SUMMARIZATION")}}
3041
- f"/summaries/{actor_id}": RetrievalConfig(top_k=3, relevance_score=0.5),
3286
+ f"/summaries/{actor_id}/{session_id}": RetrievalConfig(top_k=3, relevance_score=0.5),
3042
3287
  {{/if}}
3043
3288
  }
3044
3289
  {{/if}}
@@ -3467,7 +3712,7 @@ dependencies = [
3467
3712
  "autogen-ext[mcp] >= 0.7.5",
3468
3713
  "opentelemetry-distro",
3469
3714
  "opentelemetry-exporter-otlp",
3470
- "bedrock-agentcore >= 1.0.3",
3715
+ "bedrock-agentcore >= 1.8.0",
3471
3716
  "botocore[crt] >= 1.35.0",
3472
3717
  "tiktoken",
3473
3718
  {{#if (eq modelProvider "Bedrock")}}
@@ -3791,20 +4036,20 @@ def get_all_gateway_mcp_toolsets() -> list[MCPToolset]:
3791
4036
  {{#each gatewayProviders}}
3792
4037
  url = os.environ.get("{{envVarName}}")
3793
4038
  if url:
3794
- {{#if (eq authType "AWS_IAM")}}
4039
+ {{#if (eq authType "AWS_IAM")}}
3795
4040
  session = create_aws_session()
3796
4041
  auth = SigV4HTTPXAuth(session.get_credentials(), "bedrock-agentcore", session.region_name)
3797
- toolsets.append(MCPToolset(connection_params=StreamableHTTPConnectionParams(
4042
+ toolsets.append(MCPToolset(tool_name_prefix="{{snakeCase name}}", connection_params=StreamableHTTPConnectionParams(
3798
4043
  url=url,
3799
4044
  httpx_client_factory=lambda **kwargs: httpx.AsyncClient(auth=auth, **kwargs)
3800
4045
  )))
3801
- {{else if (eq authType "CUSTOM_JWT")}}
4046
+ {{else if (eq authType "CUSTOM_JWT")}}
3802
4047
  token = _get_bearer_token_{{snakeCase name}}()
3803
4048
  headers = {"Authorization": f"Bearer {token}"} if token else None
3804
- toolsets.append(MCPToolset(connection_params=StreamableHTTPConnectionParams(url=url, headers=headers)))
3805
- {{else}}
3806
- toolsets.append(MCPToolset(connection_params=StreamableHTTPConnectionParams(url=url)))
3807
- {{/if}}
4049
+ toolsets.append(MCPToolset(tool_name_prefix="{{snakeCase name}}", connection_params=StreamableHTTPConnectionParams(url=url, headers=headers)))
4050
+ {{else}}
4051
+ toolsets.append(MCPToolset(tool_name_prefix="{{snakeCase name}}", connection_params=StreamableHTTPConnectionParams(url=url)))
4052
+ {{/if}}
3808
4053
  else:
3809
4054
  logger.warning("{{envVarName}} not set — {{name}} gateway tools unavailable")
3810
4055
  {{/each}}
@@ -3897,7 +4142,7 @@ requires-python = ">=3.10"
3897
4142
  dependencies = [
3898
4143
  "opentelemetry-distro",
3899
4144
  "opentelemetry-exporter-otlp",
3900
- "google-adk >= 1.17.0, < 2.0.0",
4145
+ "google-adk >= 1.35.0, < 2.0.0",
3901
4146
  "google-genai >= 1.0.0, < 2.0.0",
3902
4147
  "bedrock-agentcore >= 1.0.3",
3903
4148
  "botocore[crt] >= 1.35.0",
@@ -4226,23 +4471,25 @@ def get_all_gateway_mcp_client() -> MultiServerMCPClient | None:
4226
4471
  {{#each gatewayProviders}}
4227
4472
  url = os.environ.get("{{envVarName}}")
4228
4473
  if url:
4229
- {{#if (eq authType "AWS_IAM")}}
4474
+ {{#if (eq authType "AWS_IAM")}}
4230
4475
  session = create_aws_session()
4231
4476
  auth = SigV4HTTPXAuth(session.get_credentials(), "bedrock-agentcore", session.region_name)
4232
- servers["{{name}}"] = {"transport": "streamable_http", "url": url, "auth": auth}
4233
- {{else if (eq authType "CUSTOM_JWT")}}
4477
+ servers["{{snakeCase name}}"] = {"transport": "streamable_http", "url": url, "auth": auth}
4478
+ {{else if (eq authType "CUSTOM_JWT")}}
4234
4479
  token = _get_bearer_token_{{snakeCase name}}()
4235
4480
  headers = {"Authorization": f"Bearer {token}"} if token else None
4236
- servers["{{name}}"] = {"transport": "streamable_http", "url": url, "headers": headers}
4237
- {{else}}
4238
- servers["{{name}}"] = {"transport": "streamable_http", "url": url}
4239
- {{/if}}
4481
+ servers["{{snakeCase name}}"] = {"transport": "streamable_http", "url": url, "headers": headers}
4482
+ {{else}}
4483
+ servers["{{snakeCase name}}"] = {"transport": "streamable_http", "url": url}
4484
+ {{/if}}
4240
4485
  else:
4241
4486
  logger.warning("{{envVarName}} not set — {{name}} gateway tools unavailable")
4242
4487
  {{/each}}
4243
4488
  if not servers:
4244
4489
  return None
4245
- return MultiServerMCPClient(servers)
4490
+ # tool_name_prefix namespaces each gateway's tools by server key so multiple
4491
+ # gateways exposing the same tool (e.g. x_amz_bedrock_agentcore_search) don't collide.
4492
+ return MultiServerMCPClient(servers, tool_name_prefix=True)
4246
4493
  {{else}}
4247
4494
  {{#if isVpc}}
4248
4495
  # VPC mode: external MCP endpoints are not reachable without a NAT gateway.
@@ -4420,9 +4667,9 @@ dependencies = [
4420
4667
  "opentelemetry-instrumentation-langchain >= 0.59.0",
4421
4668
  "langgraph >= 1.0.2",
4422
4669
  "mcp >= 1.19.0",
4423
- "langchain-mcp-adapters >= 0.1.11",
4670
+ "langchain-mcp-adapters >= 0.2.0",
4424
4671
  "langchain >= 1.0.3",
4425
- "bedrock-agentcore >= 1.0.3",
4672
+ "bedrock-agentcore >= 1.8.0",
4426
4673
  "botocore[crt] >= 1.35.0",
4427
4674
  {{#if (eq modelProvider "Bedrock")}}
4428
4675
  "langchain-aws >= 1.0.0",
@@ -4538,6 +4785,9 @@ exports[`Assets Directory Snapshots > Python framework assets > python/python/ht
4538
4785
  "{{#if needsOs}}
4539
4786
  import os
4540
4787
  {{/if}}
4788
+ {{#if hasGateway}}
4789
+ from contextlib import AsyncExitStack
4790
+ {{/if}}
4541
4791
  from agents import Agent, Runner, function_tool
4542
4792
  from bedrock_agentcore.runtime import BedrockAgentCoreApp
4543
4793
  from model.load import load_model
@@ -4648,15 +4898,22 @@ async def main(query):
4648
4898
  try:
4649
4899
  {{#if hasGateway}}
4650
4900
  if mcp_servers:
4651
- agent = Agent(
4652
- name="{{ name }}",
4653
- model="gpt-4.1",
4654
- instructions=INSTRUCTIONS,
4655
- mcp_servers=mcp_servers,
4656
- tools=tools
4657
- )
4658
- result = await Runner.run(agent, query)
4659
- return result
4901
+ # Connect every gateway server before the run; include_server_in_tool_names
4902
+ # namespaces each server's tools so multiple gateways exposing the same tool
4903
+ # (e.g. x_amz_bedrock_agentcore_search) don't collide.
4904
+ async with AsyncExitStack() as stack:
4905
+ for server in mcp_servers:
4906
+ await stack.enter_async_context(server)
4907
+ agent = Agent(
4908
+ name="{{ name }}",
4909
+ model="gpt-4.1",
4910
+ instructions=INSTRUCTIONS,
4911
+ mcp_servers=mcp_servers,
4912
+ tools=tools,
4913
+ mcp_config={"include_server_in_tool_names": True},
4914
+ )
4915
+ result = await Runner.run(agent, query)
4916
+ return result
4660
4917
  else:
4661
4918
  agent = Agent(
4662
4919
  name="{{ name }}",
@@ -4756,20 +5013,20 @@ def get_all_gateway_mcp_servers() -> list[MCPServerStreamableHttp]:
4756
5013
  {{#each gatewayProviders}}
4757
5014
  url = os.environ.get("{{envVarName}}")
4758
5015
  if url:
4759
- {{#if (eq authType "AWS_IAM")}}
5016
+ {{#if (eq authType "AWS_IAM")}}
4760
5017
  session = create_aws_session()
4761
5018
  auth = SigV4HTTPXAuth(session.get_credentials(), "bedrock-agentcore", session.region_name)
4762
5019
  servers.append(MCPServerStreamableHttp(
4763
5020
  name="{{name}}",
4764
5021
  params={"url": url, "httpx_client_factory": lambda **kwargs: httpx.AsyncClient(auth=auth, **kwargs)}
4765
5022
  ))
4766
- {{else if (eq authType "CUSTOM_JWT")}}
5023
+ {{else if (eq authType "CUSTOM_JWT")}}
4767
5024
  token = _get_bearer_token_{{snakeCase name}}()
4768
5025
  headers = {"Authorization": f"Bearer {token}"} if token else {}
4769
5026
  servers.append(MCPServerStreamableHttp(name="{{name}}", params={"url": url, "headers": headers}))
4770
- {{else}}
5027
+ {{else}}
4771
5028
  servers.append(MCPServerStreamableHttp(name="{{name}}", params={"url": url}))
4772
- {{/if}}
5029
+ {{/if}}
4773
5030
  else:
4774
5031
  logger.warning("{{envVarName}} not set — {{name}} gateway tools unavailable")
4775
5032
  {{/each}}
@@ -4857,8 +5114,8 @@ readme = "README.md"
4857
5114
  requires-python = ">=3.10"
4858
5115
  dependencies = [
4859
5116
  "aws-opentelemetry-distro",
4860
- "openai-agents >= 0.4.2",
4861
- "bedrock-agentcore >= 1.0.3",
5117
+ "openai-agents >= 0.16.0",
5118
+ "bedrock-agentcore >= 1.8.0",
4862
5119
  "botocore[crt] >= 1.35.0",
4863
5120
  {{#if hasGateway}}{{#if (includes gatewayAuthTypes "AWS_IAM")}}"mcp-proxy-for-aws >= 1.1.0",
4864
5121
  {{/if}}{{/if}}
@@ -4958,45 +5215,113 @@ Thumbs.db"
4958
5215
 
4959
5216
  exports[`Assets Directory Snapshots > Python framework assets > python/python/http/strands/base/main.py should match snapshot 1`] = `
4960
5217
  "from typing import Any
5218
+ {{#if inlineFunctionTools}}
5219
+ import json
4961
5220
 
5221
+ from strands.tools.tools import PythonAgentTool
5222
+ from strands.types.tools import ToolResult, ToolUse
5223
+ {{/if}}
4962
5224
  from strands import Agent, tool
5225
+ {{#if hasSkillsFetcher}}
5226
+ from strands import AgentSkills
5227
+ {{#if hasFetchedSkills}}
5228
+ from skills.fetcher import resolve_s3_skills, resolve_git_skills
5229
+ {{/if}}
5230
+ {{#if (some gitSkills "credentialArn")}}
5231
+ from bedrock_agentcore.services.identity import IdentityClient
5232
+ {{/if}}
5233
+ {{/if}}
5234
+ import asyncio
5235
+ {{#if hasShell}}
5236
+ import subprocess
5237
+ {{/if}}
5238
+ {{#if hasFileOperations}}
5239
+ import os
5240
+ {{/if}}
5241
+ {{#if hasExecutionLimits}}
5242
+ from strands.tools.executors import SequentialToolExecutor
5243
+ from strands.types.exceptions import EventLoopException
5244
+ from hooks.execution_limits import ExecutionLimitExceeded, ExecutionLimitsHook
5245
+ {{/if}}
4963
5246
  {{#if hasConfigBundle}}
4964
5247
  from strands.hooks import HookProvider, HookRegistry, BeforeInvocationEvent, BeforeToolCallEvent
5248
+ {{/if}}
5249
+ {{#if truncationStrategy}}
5250
+ {{#if (eq truncationStrategy "sliding_window")}}
5251
+ from strands.agent.conversation_manager.sliding_window_conversation_manager import SlidingWindowConversationManager
5252
+ {{/if}}
5253
+ {{#if (eq truncationStrategy "summarization")}}
5254
+ from strands.agent.conversation_manager.summarizing_conversation_manager import SummarizingConversationManager
5255
+ {{/if}}
5256
+ {{else}}
5257
+ from strands.agent.conversation_manager.null_conversation_manager import NullConversationManager
5258
+ {{/if}}
5259
+ {{#if hasConfigBundle}}
4965
5260
  from bedrock_agentcore.runtime.context import BedrockAgentCoreContext
4966
5261
  {{/if}}
5262
+ {{#if hasBrowser}}
5263
+ from strands_tools.browser import AgentCoreBrowser
5264
+ {{/if}}
5265
+ {{#if hasCodeInterpreter}}
5266
+ from strands_tools.code_interpreter import AgentCoreCodeInterpreter
5267
+ {{/if}}
4967
5268
  from bedrock_agentcore.runtime import BedrockAgentCoreApp
4968
5269
  from model.load import load_model
4969
5270
  {{#if hasGateway}}
4970
5271
  from mcp_client.client import get_all_gateway_mcp_clients
4971
- {{else}}
4972
- from mcp_client.client import get_streamable_http_mcp_client
4973
5272
  {{/if}}
5273
+ {{#if remoteMcpTools}}
5274
+ from mcp_client.client import get_all_remote_mcp_clients
5275
+ {{/if}}
5276
+ {{#unless (or hasGateway remoteMcpTools)}}
5277
+ {{#unless isExportHarness}}
5278
+ from mcp_client.client import get_streamable_http_mcp_client
5279
+ {{/unless}}
5280
+ {{/unless}}
4974
5281
  {{#if hasMemory}}
4975
5282
  from memory.session import get_memory_session_manager
4976
5283
  {{/if}}
4977
- {{#if needsOs}}
5284
+ {{#unless hasFileOperations}}
5285
+ {{#if (or needsOs (some gitSkills "credentialArn"))}}
4978
5286
  import os
4979
5287
  {{/if}}
5288
+ {{/unless}}
5289
+ {{#if hasPayment}}
5290
+ from capabilities.payments.payments import create_payments_plugin, PAYMENT_SYSTEM_PROMPT
5291
+ {{/if}}
4980
5292
 
4981
5293
  app = BedrockAgentCoreApp()
4982
5294
  log = app.logger
4983
5295
 
4984
- # Define a Streamable HTTP MCP Client
5296
+ {{#if (or hasGateway remoteMcpTools)}}
5297
+ # Define MCP clients for all configured MCP servers (gateways and/or remote MCP)
5298
+ mcp_clients = []
4985
5299
  {{#if hasGateway}}
4986
- mcp_clients = get_all_gateway_mcp_clients()
5300
+ mcp_clients += get_all_gateway_mcp_clients()
5301
+ {{/if}}
5302
+ {{#if remoteMcpTools}}
5303
+ mcp_clients += get_all_remote_mcp_clients()
5304
+ {{/if}}
4987
5305
  {{else}}
5306
+ {{#unless isExportHarness}}
5307
+ # Define a Streamable HTTP MCP Client
4988
5308
  mcp_clients = [get_streamable_http_mcp_client()]
5309
+ {{/unless}}
4989
5310
  {{/if}}
4990
5311
 
5312
+ {{#if systemPromptText}}
5313
+ DEFAULT_SYSTEM_PROMPT = """{{escapePyStr systemPromptText}}"""
5314
+ {{else}}
4991
5315
  DEFAULT_SYSTEM_PROMPT = """
4992
5316
  You are a helpful assistant. Use tools when appropriate.
4993
- {{#if needsOs}}
5317
+ {{#if needsOs}}{{#unless isExportHarness}}
4994
5318
  You have access to the following mounted filesystems. Use file_read, file_write, and list_files with full absolute paths:
4995
5319
  {{#if sessionStorageMountPath}}- {{sessionStorageMountPath}}: ephemeral session storage (lost when session ends)
4996
5320
  {{/if}}{{#each efsMounts}}- {{mountPath}}: EFS persistent storage (persists across sessions and agent restarts)
4997
5321
  {{/each}}{{#each s3Mounts}}- {{mountPath}}: S3 Files persistent storage (durable, backed by S3)
4998
- {{/each}}{{/if}}
5322
+ {{/each}}{{/unless}}{{/if}}
4999
5323
  """
5324
+ {{/if}}
5000
5325
 
5001
5326
  {{#if hasConfigBundle}}
5002
5327
  DEFAULT_TOOL_DESC = "Return the sum of two numbers"
@@ -5005,6 +5330,30 @@ DEFAULT_TOOL_DESC = "Return the sum of two numbers"
5005
5330
  # Define a collection of tools used by the model
5006
5331
  tools = []
5007
5332
 
5333
+ {{#if inlineFunctionTools}}
5334
+ # Inline function tools — stop the agent loop so the tool call streams back to the caller
5335
+ def _make_inline_tool(name: str, spec: dict) -> PythonAgentTool:
5336
+ def _handler(tool: ToolUse, **kwargs: Any) -> ToolResult:
5337
+ kwargs.get("request_state", {})["stop_event_loop"] = True
5338
+ return {"toolUseId": tool["toolUseId"], "status": "success", "content": [{"text": " "}]}
5339
+ _handler.__name__ = name
5340
+ return PythonAgentTool(tool_name=name, tool_spec=spec, tool_func=_handler)
5341
+
5342
+ {{#each inlineFunctionTools}}
5343
+ _INLINE_SPEC_{{snakeCase name}} = {
5344
+ "name": "{{name}}",
5345
+ "description": {{safeJson description}},
5346
+ "inputSchema": {"json": json.loads({{pyJsonStr inputSchema}}) },
5347
+ }
5348
+ tools.append(_make_inline_tool("{{name}}", _INLINE_SPEC_{{snakeCase name}}))
5349
+ {{/each}}
5350
+
5351
+ _INLINE_FUNCTION_NAMES = { {{#each inlineFunctionTools}}"{{name}}"{{#unless @last}}, {{/unless}}{{/each}} }
5352
+
5353
+ {{else}}
5354
+ _INLINE_FUNCTION_NAMES = set()
5355
+
5356
+ {{#unless isExportHarness}}
5008
5357
  # Define a simple function tool
5009
5358
  {{#if hasConfigBundle}}
5010
5359
  @tool(description=DEFAULT_TOOL_DESC)
@@ -5016,7 +5365,116 @@ def add_numbers(a: int, b: int) -> int:
5016
5365
  return a+b
5017
5366
  tools.append(add_numbers)
5018
5367
 
5019
- {{#if needsOs}}
5368
+ {{/unless}}
5369
+ {{/if}}
5370
+ {{#if hasBrowser}}
5371
+ tools.append(AgentCoreBrowser({{#if browserIdentifier}}identifier="{{browserIdentifier}}"{{/if}}).browser)
5372
+ {{/if}}
5373
+ {{#if hasCodeInterpreter}}
5374
+ tools.append(AgentCoreCodeInterpreter({{#if codeInterpreterIdentifier}}identifier="{{codeInterpreterIdentifier}}"{{/if}}).code_interpreter)
5375
+ {{/if}}
5376
+ {{#if hasShell}}
5377
+ @tool
5378
+ def shell(command: str, timeout: int = 300) -> dict:
5379
+ """Execute a bash command and return the results.
5380
+
5381
+ Args:
5382
+ command: The bash command to execute
5383
+ timeout: Timeout in seconds (default: 300)
5384
+
5385
+ Returns:
5386
+ Dict with stdout, stderr, and exit_code
5387
+ """
5388
+ result = subprocess.run(
5389
+ command, shell=True, capture_output=True, text=True, timeout=timeout
5390
+ )
5391
+ return {"stdout": result.stdout, "stderr": result.stderr, "exit_code": result.returncode}
5392
+
5393
+ tools.append(shell)
5394
+ {{/if}}
5395
+ {{#if hasFileOperations}}
5396
+ @tool
5397
+ def file_operations(
5398
+ command: str,
5399
+ path: str,
5400
+ old_str: str = None,
5401
+ new_str: str = None,
5402
+ file_text: str = None,
5403
+ insert_line: int = None,
5404
+ view_range: list = None,
5405
+ ) -> str:
5406
+ """Text editor tool for viewing and modifying files.
5407
+
5408
+ Args:
5409
+ command: The command to execute ("view", "str_replace", "create", "insert")
5410
+ path: Path to the file or directory
5411
+ old_str: Text to replace (for str_replace command)
5412
+ new_str: Replacement text (for str_replace and insert commands)
5413
+ file_text: Content for new file (for create command)
5414
+ insert_line: Line number to insert after (for insert command)
5415
+ view_range: [start_line, end_line] for viewing specific lines (for view command)
5416
+
5417
+ Returns:
5418
+ Result of the operation
5419
+ """
5420
+ try:
5421
+ if command == "view":
5422
+ if not os.path.exists(path):
5423
+ return f"Error: Path '{path}' does not exist"
5424
+ if os.path.isdir(path):
5425
+ return "\\n".join(os.listdir(path))
5426
+ with open(path) as f:
5427
+ lines = f.read().splitlines()
5428
+ if view_range:
5429
+ start, end = view_range
5430
+ start_idx = max(0, start - 1)
5431
+ end_idx = len(lines) if end == -1 else min(len(lines), end)
5432
+ lines = lines[start_idx:end_idx]
5433
+ start_num = start_idx + 1
5434
+ else:
5435
+ start_num = 1
5436
+ return "\\n".join(f"{start_num + i}: {line}" for i, line in enumerate(lines))
5437
+ elif command == "str_replace":
5438
+ if old_str is None or new_str is None:
5439
+ return "Error: str_replace requires both old_str and new_str parameters"
5440
+ if not os.path.exists(path):
5441
+ return f"Error: File '{path}' does not exist"
5442
+ content = open(path).read()
5443
+ if old_str not in content:
5444
+ return "Error: Text not found in file"
5445
+ count = content.count(old_str)
5446
+ if count > 1:
5447
+ return f"Error: Text appears {count} times in file. Please be more specific."
5448
+ open(path, "w").write(content.replace(old_str, new_str, 1))
5449
+ return f"Successfully replaced text in '{path}'"
5450
+ elif command == "create":
5451
+ if file_text is None:
5452
+ return "Error: create requires file_text parameter"
5453
+ os.makedirs(os.path.dirname(os.path.abspath(path)), exist_ok=True)
5454
+ open(path, "w").write(file_text)
5455
+ return f"Successfully created file '{path}'"
5456
+ elif command == "insert":
5457
+ if new_str is None or insert_line is None:
5458
+ return "Error: insert requires both new_str and insert_line parameters"
5459
+ if not os.path.exists(path):
5460
+ return f"Error: File '{path}' does not exist"
5461
+ lines = open(path).read().splitlines(True)
5462
+ if insert_line == 0:
5463
+ lines.insert(0, new_str + "\\n")
5464
+ elif insert_line >= len(lines):
5465
+ lines.append(new_str + "\\n")
5466
+ else:
5467
+ lines.insert(insert_line, new_str + "\\n")
5468
+ open(path, "w").write("".join(lines))
5469
+ return f"Successfully inserted text in '{path}' at line {insert_line + 1}"
5470
+ else:
5471
+ return f"Error: Unknown command '{command}'"
5472
+ except Exception as e:
5473
+ return f"Error: {e}"
5474
+
5475
+ tools.append(file_operations)
5476
+ {{/if}}
5477
+ {{#if needsOs}}{{#unless isExportHarness}}
5020
5478
  _MOUNT_PATHS = [
5021
5479
  {{#if sessionStorageMountPath}}"{{sessionStorageMountPath}}",{{/if}}
5022
5480
  {{#each efsMounts}}"{{mountPath}}",{{/each}}
@@ -5070,12 +5528,21 @@ def list_files(path: str) -> str:
5070
5528
  return f"Error listing '{path}': {e.strerror}"
5071
5529
 
5072
5530
  tools.extend([file_read, file_write, list_files])
5073
- {{/if}}
5531
+ {{/unless}}{{/if}}
5074
5532
 
5533
+ {{#if (or hasGateway remoteMcpTools)}}
5534
+ # Add MCP clients to tools
5535
+ for mcp_client in mcp_clients:
5536
+ if mcp_client:
5537
+ tools.append(mcp_client)
5538
+ {{else}}
5539
+ {{#unless isExportHarness}}
5075
5540
  # Add MCP client to tools if available
5076
5541
  for mcp_client in mcp_clients:
5077
5542
  if mcp_client:
5078
5543
  tools.append(mcp_client)
5544
+ {{/unless}}
5545
+ {{/if}}
5079
5546
 
5080
5547
  {{#if hasConfigBundle}}
5081
5548
 
@@ -5111,71 +5578,309 @@ class ConfigBundleHook(HookProvider):
5111
5578
 
5112
5579
  {{/if}}
5113
5580
 
5581
+ def _make_conversation_manager():
5582
+ {{#if truncationStrategy}}
5583
+ {{#if (eq truncationStrategy "sliding_window")}}
5584
+ {{#if truncationConfig}}
5585
+ return SlidingWindowConversationManager(**{{safeJson truncationConfig}}, per_turn=True)
5586
+ {{else}}
5587
+ return SlidingWindowConversationManager(per_turn=True)
5588
+ {{/if}}
5589
+ {{else}}
5590
+ {{#if truncationConfig}}
5591
+ return SummarizingConversationManager(**{{safeJson truncationConfig}})
5592
+ {{else}}
5593
+ return SummarizingConversationManager()
5594
+ {{/if}}
5595
+ {{/if}}
5596
+ {{else}}
5597
+ return NullConversationManager()
5598
+ {{/if}}
5599
+
5114
5600
  {{#if hasMemory}}
5601
+ {{#unless hasPayment}}
5115
5602
  def agent_factory():
5116
5603
  cache = {}
5117
- def get_or_create_agent(session_id, user_id):
5118
- key = f"{session_id}/{user_id}"
5604
+ def get_or_create_agent(session_id, user_id{{#if hasSkillsFetcher}}, skill_plugins=None{{/if}}):
5605
+ {{#if actorId}}
5606
+ _actor_id = "{{actorId}}"
5607
+ {{else}}
5608
+ _actor_id = user_id
5609
+ {{/if}}
5610
+ key = f"{session_id}/{_actor_id}"
5119
5611
  if key not in cache:
5120
- # Create an agent for the given session_id and user_id
5121
5612
  cache[key] = Agent(
5122
5613
  model=load_model(),
5123
- session_manager=get_memory_session_manager(session_id, user_id),
5614
+ session_manager=get_memory_session_manager(session_id, _actor_id),
5615
+ conversation_manager=_make_conversation_manager(),
5124
5616
  system_prompt=DEFAULT_SYSTEM_PROMPT,
5125
- tools=tools{{#if hasConfigBundle}},
5126
- hooks=[ConfigBundleHook()]{{/if}}
5617
+ tools=tools,
5618
+ {{#if hasSkillsFetcher}}
5619
+ plugins=skill_plugins or None,
5620
+ {{/if}}
5621
+ {{#if hasExecutionLimits}}
5622
+ tool_executor=SequentialToolExecutor(),
5623
+ callback_handler=None,
5624
+ {{/if}}
5625
+ hooks=[
5626
+ {{#if hasExecutionLimits}}
5627
+ ExecutionLimitsHook(
5628
+ {{#if maxIterations}}max_iterations={{maxIterations}},{{/if}}
5629
+ {{#if maxTokens}}max_tokens={{maxTokens}},{{/if}}
5630
+ {{#if timeoutSeconds}}timeout_seconds={{timeoutSeconds}},{{/if}}
5631
+ ),
5632
+ {{/if}}
5633
+ {{#if hasConfigBundle}}
5634
+ ConfigBundleHook(),
5635
+ {{/if}}
5636
+ ],
5127
5637
  )
5128
5638
  return cache[key]
5129
5639
  return get_or_create_agent
5130
5640
  get_or_create_agent = agent_factory()
5641
+ {{/unless}}
5131
5642
  {{else}}
5132
5643
  {{#if hasConfigBundle}}
5133
- def create_agent():
5644
+ def create_agent({{#if hasSkillsFetcher}}skill_plugins=None{{/if}}):
5134
5645
  return Agent(
5135
5646
  model=load_model(),
5136
5647
  system_prompt=DEFAULT_SYSTEM_PROMPT,
5137
5648
  tools=tools,
5649
+ conversation_manager=_make_conversation_manager(),
5650
+ {{#if hasSkillsFetcher}}
5651
+ plugins=skill_plugins or None,
5652
+ {{/if}}
5138
5653
  hooks=[ConfigBundleHook()],
5139
5654
  )
5140
5655
  {{else}}
5656
+ {{#unless hasPayment}}
5141
5657
  _agent = None
5142
5658
 
5143
- def get_or_create_agent():
5659
+ def get_or_create_agent({{#if hasSkillsFetcher}}skill_plugins=None{{/if}}):
5144
5660
  global _agent
5145
5661
  if _agent is None:
5146
5662
  _agent = Agent(
5147
5663
  model=load_model(),
5148
5664
  system_prompt=DEFAULT_SYSTEM_PROMPT,
5149
- tools=tools
5665
+ tools=tools,
5666
+ conversation_manager=_make_conversation_manager(),
5667
+ {{#if hasSkillsFetcher}}
5668
+ plugins=skill_plugins or None,
5669
+ {{/if}}
5670
+ {{#if hasExecutionLimits}}
5671
+ tool_executor=SequentialToolExecutor(),
5672
+ callback_handler=None,
5673
+ {{/if}}
5674
+ hooks=[
5675
+ {{#if hasExecutionLimits}}
5676
+ ExecutionLimitsHook(
5677
+ {{#if maxIterations}}max_iterations={{maxIterations}},{{/if}}
5678
+ {{#if maxTokens}}max_tokens={{maxTokens}},{{/if}}
5679
+ {{#if timeoutSeconds}}timeout_seconds={{timeoutSeconds}},{{/if}}
5680
+ ),
5681
+ {{/if}}
5682
+ ],
5150
5683
  )
5151
5684
  return _agent
5685
+ {{/unless}}
5152
5686
  {{/if}}
5153
5687
  {{/if}}
5154
5688
 
5155
5689
 
5690
+ def _extract_prompt(payload: dict):
5691
+ """Accept harness-style messages[], tool_results[], or plain prompt string payloads."""
5692
+ if "messages" in payload:
5693
+ return payload["messages"]
5694
+ if "tool_results" in payload:
5695
+ return [{"role": "user", "content": [{"toolResult": {
5696
+ "toolUseId": tr["toolUseId"],
5697
+ "status": tr.get("status", "success"),
5698
+ "content": tr.get("content", []),
5699
+ }} for tr in payload["tool_results"]]}]
5700
+ return payload.get("prompt", "")
5701
+
5702
+
5703
+ def _has_inline_function_call(messages) -> bool:
5704
+ """Return True if messages contains an assistant toolUse for an inline function tool."""
5705
+ if not _INLINE_FUNCTION_NAMES or not isinstance(messages, list):
5706
+ return False
5707
+ for msg in messages:
5708
+ if msg.get("role") == "assistant":
5709
+ for block in msg.get("content", []):
5710
+ if isinstance(block, dict) and block.get("toolUse", {}).get("name") in _INLINE_FUNCTION_NAMES:
5711
+ return True
5712
+ return False
5713
+
5714
+
5715
+ def _is_inline_function_call(event: dict) -> bool:
5716
+ """Check if a contentBlockStart event is for an inline function tool."""
5717
+ if not _INLINE_FUNCTION_NAMES:
5718
+ return False
5719
+ cbs = event.get("contentBlockStart", {})
5720
+ start = cbs.get("start", {})
5721
+ tool_use = start.get("toolUse") if isinstance(start, dict) else None
5722
+ return tool_use is not None and tool_use.get("name") in _INLINE_FUNCTION_NAMES
5723
+
5724
+
5725
+
5156
5726
  @app.entrypoint
5157
5727
  async def invoke(payload, context):
5158
5728
  log.info("Invoking Agent.....")
5159
5729
 
5730
+ {{#if hasPayment}}
5731
+ user_id = payload.get("user_id") or getattr(context, "user_id", "default-user")
5732
+ instrument_id = payload.get("payment_instrument_id")
5733
+ session_id = payload.get("payment_session_id")
5734
+ payments_plugin = create_payments_plugin(user_id, instrument_id, session_id)
5735
+ plugins = [payments_plugin] if payments_plugin else []
5736
+ {{/if}}
5737
+ {{#if hasSkillsFetcher}}
5738
+ skill_paths = [{{#each pathSkills}}{{safeJson this}}{{#unless @last}}, {{/unless}}{{/each}}]
5739
+ {{#if s3Skills}}
5740
+ s3_skill_sources = [{{#each s3Skills}}{{safeJson this}}{{#unless @last}}, {{/unless}}{{/each}}]
5741
+ skill_paths.extend(await asyncio.to_thread(resolve_s3_skills, s3_skill_sources, None))
5742
+ {{/if}}
5743
+ {{#if gitSkills}}
5744
+ git_skill_sources = [
5745
+ {{#each gitSkills}}
5746
+ 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}}),
5747
+ {{/each}}
5748
+ ]
5749
+ {{#if (some gitSkills "credentialArn")}}
5750
+ _git_identity_client = IdentityClient(os.environ.get("AWS_REGION", os.environ.get("AWS_DEFAULT_REGION", "us-east-1")))
5751
+ {{else}}
5752
+ _git_identity_client = None
5753
+ {{/if}}
5754
+ skill_paths.extend(await asyncio.to_thread(resolve_git_skills, git_skill_sources, _git_identity_client))
5755
+ {{/if}}
5756
+ _skill_plugins = [AgentSkills(skills=skill_paths)] if skill_paths else []
5757
+ {{/if}}
5758
+
5160
5759
  {{#if hasMemory}}
5760
+ {{#if hasPayment}}
5761
+ mem_session_id = getattr(context, 'session_id', 'default-session')
5762
+ {{#if actorId}}
5763
+ mem_user_id = "{{actorId}}"
5764
+ {{else}}
5765
+ mem_user_id = getattr(context, 'user_id', 'default-user')
5766
+ {{/if}}
5767
+ agent = Agent(
5768
+ model=load_model(),
5769
+ session_manager=get_memory_session_manager(mem_session_id, mem_user_id),
5770
+ system_prompt=DEFAULT_SYSTEM_PROMPT + PAYMENT_SYSTEM_PROMPT,
5771
+ tools=tools,
5772
+ plugins=plugins{{#if hasSkillsFetcher}} + _skill_plugins{{/if}},{{#if hasConfigBundle}}
5773
+ hooks=[ConfigBundleHook()],{{/if}}
5774
+ )
5775
+ {{else}}
5161
5776
  session_id = getattr(context, 'session_id', 'default-session')
5777
+ {{#if actorId}}
5778
+ user_id = "{{actorId}}"
5779
+ {{else}}
5162
5780
  user_id = getattr(context, 'user_id', 'default-user')
5163
- agent = get_or_create_agent(session_id, user_id)
5781
+ {{/if}}
5782
+ agent = get_or_create_agent(session_id, user_id{{#if hasSkillsFetcher}}, _skill_plugins{{/if}})
5783
+ {{/if}}
5784
+ {{else}}
5785
+ {{#if hasPayment}}
5786
+ agent = Agent(
5787
+ model=load_model(),
5788
+ system_prompt=DEFAULT_SYSTEM_PROMPT + PAYMENT_SYSTEM_PROMPT,
5789
+ tools=tools,
5790
+ plugins=plugins{{#if hasSkillsFetcher}} + _skill_plugins{{/if}},{{#if hasConfigBundle}}
5791
+ hooks=[ConfigBundleHook()],{{/if}}
5792
+ )
5164
5793
  {{else}}
5165
5794
  {{#if hasConfigBundle}}
5166
- agent = create_agent()
5795
+ agent = create_agent({{#if hasSkillsFetcher}}_skill_plugins{{/if}})
5167
5796
  {{else}}
5168
- agent = get_or_create_agent()
5797
+ agent = get_or_create_agent({{#if hasSkillsFetcher}}_skill_plugins{{/if}})
5798
+ {{/if}}
5169
5799
  {{/if}}
5170
5800
  {{/if}}
5171
5801
 
5172
- # Execute and format response
5173
- stream = agent.stream_async(payload.get("prompt"))
5802
+ prompt = _extract_prompt(payload)
5174
5803
 
5175
- async for event in stream:
5176
- # Handle Text parts of the response
5177
- if "data" in event and isinstance(event["data"], str):
5178
- yield event["data"]
5804
+ {{#if inlineFunctionTools}}
5805
+ # If Turn 2 carries the harness-style assistant(toolUse)+user(toolResult) pair,
5806
+ # strip the placeholder turn Strands stored during Turn 1 so the real toolResult
5807
+ # is injected cleanly — same protocol as the harness runtime.
5808
+ if _has_inline_function_call(prompt):
5809
+ msgs = agent.messages
5810
+ if len(msgs) >= 2 and any("toolResult" in b for b in msgs[-1].get("content", [])):
5811
+ del msgs[-2:]
5812
+ {{/if}}
5813
+
5814
+ {{#if hasExecutionLimits}}
5815
+ timeout_seconds = {{#if timeoutSeconds}}{{timeoutSeconds}}{{else}}None{{/if}}
5816
+ timeout_fired = False
5817
+ watchdog_task = None
5818
+ if timeout_seconds is not None:
5819
+ async def _timeout_watchdog():
5820
+ nonlocal timeout_fired
5821
+ await asyncio.sleep(timeout_seconds)
5822
+ timeout_fired = True
5823
+ agent.cancel()
5824
+ watchdog_task = asyncio.create_task(_timeout_watchdog())
5825
+
5826
+ try:
5827
+ {{#if inlineFunctionTools}}
5828
+ hit_inline_function = False
5829
+ {{/if}}
5830
+ async for event in agent.stream_async(
5831
+ prompt,
5832
+ ):
5833
+ if not isinstance(event, dict) or "event" not in event:
5834
+ continue
5835
+ cbs = event["event"].get("contentBlockStart")
5836
+ if cbs is not None and not cbs.get("start"):
5837
+ continue
5838
+ {{#if inlineFunctionTools}}
5839
+ if not hit_inline_function:
5840
+ hit_inline_function = _is_inline_function_call(event["event"])
5841
+ {{/if}}
5842
+ yield event
5843
+ {{#if inlineFunctionTools}}
5844
+ if hit_inline_function and "messageStop" in event["event"]:
5845
+ return
5846
+ {{/if}}
5847
+
5848
+ if timeout_fired:
5849
+ yield {"event": {"messageStop": {"stopReason": "timeout_exceeded"}}}
5850
+ except EventLoopException as e:
5851
+ if isinstance(e.original_exception, ExecutionLimitExceeded):
5852
+ yield {"event": {"messageStop": {"stopReason": str(e.original_exception)}}}
5853
+ return
5854
+ raise
5855
+ finally:
5856
+ if watchdog_task is not None:
5857
+ watchdog_task.cancel()
5858
+ try:
5859
+ await watchdog_task
5860
+ except asyncio.CancelledError:
5861
+ pass
5862
+ {{else}}
5863
+ {{#if inlineFunctionTools}}
5864
+ hit_inline_function = False
5865
+ {{/if}}
5866
+ async for event in agent.stream_async(
5867
+ prompt,
5868
+ ):
5869
+ if not isinstance(event, dict) or "event" not in event:
5870
+ continue
5871
+ cbs = event["event"].get("contentBlockStart")
5872
+ if cbs is not None and not cbs.get("start"):
5873
+ continue
5874
+ {{#if inlineFunctionTools}}
5875
+ if not hit_inline_function:
5876
+ hit_inline_function = _is_inline_function_call(event["event"])
5877
+ {{/if}}
5878
+ yield event
5879
+ {{#if inlineFunctionTools}}
5880
+ if hit_inline_function and "messageStop" in event["event"]:
5881
+ return
5882
+ {{/if}}
5883
+ {{/if}}
5179
5884
 
5180
5885
 
5181
5886
  if __name__ == "__main__":
@@ -5220,18 +5925,22 @@ def _get_bearer_token_{{snakeCase name}}(*, access_token: str):
5220
5925
  {{#each gatewayProviders}}
5221
5926
  def get_{{snakeCase name}}_mcp_client() -> MCPClient | None:
5222
5927
  """Returns an MCP Client connected to the {{name}} gateway."""
5928
+ {{#if hardcodedUrl}}
5929
+ url = {{safeJson hardcodedUrl}}
5930
+ {{else}}
5223
5931
  url = os.environ.get("{{envVarName}}")
5224
5932
  if not url:
5225
5933
  logger.warning("{{envVarName}} not set — {{name}} gateway tools unavailable")
5226
5934
  return None
5935
+ {{/if}}
5227
5936
  {{#if (eq authType "AWS_IAM")}}
5228
- 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"))))
5937
+ 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}}")
5229
5938
  {{else if (eq authType "CUSTOM_JWT")}}
5230
5939
  token = _get_bearer_token_{{snakeCase name}}()
5231
5940
  headers = {"Authorization": f"Bearer {token}"} if token else {}
5232
- return MCPClient(lambda: streamablehttp_client(url, headers=headers))
5941
+ return MCPClient(lambda: streamablehttp_client(url, headers=headers), prefix="{{snakeCase name}}")
5233
5942
  {{else}}
5234
- return MCPClient(lambda: streamablehttp_client(url))
5943
+ return MCPClient(lambda: streamablehttp_client(url), prefix="{{snakeCase name}}")
5235
5944
  {{/if}}
5236
5945
 
5237
5946
  {{/each}}
@@ -5244,7 +5953,41 @@ def get_all_gateway_mcp_clients() -> list[MCPClient]:
5244
5953
  clients.append(client)
5245
5954
  {{/each}}
5246
5955
  return clients
5247
- {{else}}
5956
+ {{/if}}
5957
+ {{#if remoteMcpTools}}
5958
+ {{#if (some remoteMcpTools "headerCredentials")}}
5959
+ from bedrock_agentcore.identity.auth import requires_api_key
5960
+ {{/if}}
5961
+ {{#each remoteMcpTools}}
5962
+ {{#if headerCredentials}}
5963
+ {{#each headerCredentials}}
5964
+ @requires_api_key(provider_name="{{credentialName}}")
5965
+ def _get_{{snakeCase ../name}}_{{snakeCase headerKey}}_key(api_key: str) -> str:
5966
+ """Fetch {{headerKey}} credential for {{../name}} from AgentCore Identity."""
5967
+ return api_key
5968
+
5969
+ {{/each}}
5970
+ {{/if}}
5971
+ def get_{{snakeCase name}}_mcp_client() -> MCPClient | None:
5972
+ """Returns an MCP Client for the {{name}} remote MCP server."""
5973
+ url = {{safeJson url}}
5974
+ {{#if headerCredentials}}
5975
+ if os.getenv("LOCAL_DEV") == "1":
5976
+ headers = { {{#each headerCredentials}}{{safeJson headerKey}}: os.environ.get("{{envVarName}}", ""){{#unless @last}}, {{/unless}}{{/each}} }
5977
+ else:
5978
+ headers = { {{#each headerCredentials}}{{safeJson headerKey}}: _get_{{snakeCase ../name}}_{{snakeCase headerKey}}_key(){{#unless @last}}, {{/unless}}{{/each}} }
5979
+ return MCPClient(lambda: streamablehttp_client(url, headers=headers))
5980
+ {{else}}
5981
+ return MCPClient(lambda: streamablehttp_client(url))
5982
+ {{/if}}
5983
+
5984
+ {{/each}}
5985
+ def get_all_remote_mcp_clients() -> list[MCPClient]:
5986
+ """Returns all configured remote MCP clients."""
5987
+ clients = [{{#each remoteMcpTools}}get_{{snakeCase name}}_mcp_client(){{#unless @last}}, {{/unless}}{{/each}}]
5988
+ return [c for c in clients if c is not None]
5989
+ {{/if}}
5990
+ {{#unless (or hasGateway remoteMcpTools)}}
5248
5991
  {{#if isVpc}}
5249
5992
  # VPC mode: external MCP endpoints are not reachable without a NAT gateway.
5250
5993
  # Add an AgentCore Gateway with \`agentcore add gateway\`, or configure your own endpoint below.
@@ -5253,6 +5996,7 @@ def get_streamable_http_mcp_client() -> MCPClient | None:
5253
5996
  """No MCP server configured. Add a gateway with \`agentcore add gateway\`."""
5254
5997
  return None
5255
5998
  {{else}}
5999
+ {{#unless isExportHarness}}
5256
6000
  # ExaAI provides information about code through web searches, crawling and code context searches through their platform. Requires no authentication
5257
6001
  EXAMPLE_MCP_ENDPOINT = "https://mcp.exa.ai/mcp"
5258
6002
 
@@ -5260,8 +6004,9 @@ def get_streamable_http_mcp_client() -> MCPClient:
5260
6004
  """Returns an MCP Client compatible with Strands"""
5261
6005
  # to use an MCP server that supports bearer authentication, add headers={"Authorization": f"Bearer {access_token}"}
5262
6006
  return MCPClient(lambda: streamablehttp_client(EXAMPLE_MCP_ENDPOINT))
6007
+ {{/unless}}
5263
6008
  {{/if}}
5264
- {{/if}}
6009
+ {{/unless}}
5265
6010
  "
5266
6011
  `;
5267
6012
 
@@ -5277,7 +6022,7 @@ from strands.models.bedrock import BedrockModel
5277
6022
 
5278
6023
  def load_model() -> BedrockModel:
5279
6024
  """Get Bedrock model client using IAM credentials."""
5280
- return BedrockModel(model_id="global.anthropic.claude-sonnet-4-5-20250929-v1:0")
6025
+ return BedrockModel(model_id="{{#if modelId}}{{modelId}}{{else}}global.anthropic.claude-sonnet-4-5-20250929-v1:0{{/if}}")
5281
6026
  {{/if}}
5282
6027
  {{#if (eq modelProvider "Anthropic")}}
5283
6028
  import os
@@ -5353,7 +6098,7 @@ def load_model() -> OpenAIModel:
5353
6098
  """Get authenticated OpenAI model client."""
5354
6099
  return OpenAIModel(
5355
6100
  client_args={"api_key": _get_api_key()},
5356
- model_id="gpt-4.1",
6101
+ model_id="{{#if modelId}}{{modelId}}{{else}}gpt-4.1{{/if}}",
5357
6102
  )
5358
6103
  {{/if}}
5359
6104
  {{#if (eq modelProvider "Gemini")}}
@@ -5391,7 +6136,7 @@ def load_model() -> GeminiModel:
5391
6136
  """Get authenticated Gemini model client."""
5392
6137
  return GeminiModel(
5393
6138
  client_args={"api_key": _get_api_key()},
5394
- model_id="gemini-2.5-flash",
6139
+ model_id="{{#if modelId}}{{modelId}}{{else}}gemini-2.5-flash{{/if}}",
5395
6140
  )
5396
6141
  {{/if}}
5397
6142
  "
@@ -5416,8 +6161,11 @@ dependencies = [
5416
6161
  {{#if (eq modelProvider "Gemini")}}"google-genai >= 1.0.0",
5417
6162
  {{/if}}"mcp >= 1.19.0",
5418
6163
  {{#if (eq modelProvider "OpenAI")}}"openai >= 1.0.0",
5419
- {{/if}}"strands-agents >= 1.13.0",
5420
- {{#if hasGateway}}{{#if (includes gatewayAuthTypes "AWS_IAM")}}"mcp-proxy-for-aws >= 1.1.0",
6164
+ {{/if}}"strands-agents >= 1.15.0",
6165
+ {{#if (or hasBrowser hasCodeInterpreter)}}"strands-agents-tools >= 0.1.0",
6166
+ {{/if}}{{#if hasBrowser}}"nest-asyncio >= 1.5.0",
6167
+ "playwright >= 1.42.0",
6168
+ {{/if}}{{#if hasGateway}}{{#if (includes gatewayAuthTypes "AWS_IAM")}}"mcp-proxy-for-aws >= 1.1.0",
5421
6169
  {{/if}}{{/if}}
5422
6170
  ]
5423
6171
 
@@ -5426,6 +6174,347 @@ packages = ["."]
5426
6174
  "
5427
6175
  `;
5428
6176
 
6177
+ exports[`Assets Directory Snapshots > Python framework assets > python/python/http/strands/base/skills/fetcher.py should match snapshot 1`] = `
6178
+ """"Skill fetcher — downloads s3/git skills to local filesystem on first use.
6179
+
6180
+ Resolved paths are passed to AgentSkills(skills=...) in main.py.
6181
+ Cache directory: <tmpdir>/.agents/skills/ — an absolute path under the system temp
6182
+ directory (honors $TMPDIR, defaults to /tmp). The runtime working directory (e.g.
6183
+ /var/task in a CodeZip runtime) is read-only, so the cache must live somewhere
6184
+ guaranteed-writable.
6185
+ """
6186
+
6187
+ import base64
6188
+ import hashlib
6189
+ import json
6190
+ import logging
6191
+ import os
6192
+ import shutil
6193
+ import subprocess
6194
+ import tempfile
6195
+ from pathlib import Path
6196
+ from typing import Optional
6197
+
6198
+ logger = logging.getLogger(__name__)
6199
+
6200
+ _SKILLS_BASE = Path(tempfile.gettempdir()) / ".agents" / "skills"
6201
+ _GIT_TIMEOUT = 60
6202
+ _S3_MAX_SIZE_BYTES = 1 * 1024 * 1024 * 1024 # 1 GB
6203
+
6204
+
6205
+ def _stable_hash(value: str) -> str:
6206
+ return hashlib.sha256(value.encode()).hexdigest()[:12]
6207
+
6208
+
6209
+ def _cleanup(path: Path) -> None:
6210
+ """Remove a partially-created skill directory so retries don't see stale state."""
6211
+ shutil.rmtree(path, ignore_errors=True)
6212
+
6213
+
6214
+ def _read_map(type_dir: Path) -> dict:
6215
+ map_file = type_dir / ".map.json"
6216
+ return json.loads(map_file.read_text()) if map_file.exists() else {}
6217
+
6218
+
6219
+ def _write_map(type_dir: Path, mapping: dict) -> None:
6220
+ type_dir.mkdir(parents=True, exist_ok=True)
6221
+ (type_dir / ".map.json").write_text(json.dumps(mapping))
6222
+
6223
+
6224
+ def _resolve_cached(type_dir: Path, source_hash: str) -> Optional[str]:
6225
+ """Return the cached skill directory for a source hash, or None if not on disk."""
6226
+ mapping = _read_map(type_dir)
6227
+ dir_name = mapping.get(source_hash)
6228
+ if dir_name and (type_dir / dir_name).exists():
6229
+ return str(type_dir / dir_name)
6230
+ return None
6231
+
6232
+
6233
+ def _read_skill_name(skill_dir: Path) -> str:
6234
+ """Extract the skill name from SKILL.md YAML frontmatter."""
6235
+ content = (skill_dir / "SKILL.md").read_text()
6236
+ if not content.startswith("---"):
6237
+ raise ValueError(f"SKILL.md in {skill_dir} has no YAML frontmatter (must start with ---)")
6238
+ parts = content.split("---", 2)
6239
+ if len(parts) < 3:
6240
+ raise ValueError(f"SKILL.md in {skill_dir} has malformed frontmatter (missing closing ---)")
6241
+ for line in parts[1].strip().splitlines():
6242
+ if line.startswith("name:"):
6243
+ name = line[len("name:"):].strip().strip("\\"'")
6244
+ if name:
6245
+ return name
6246
+ raise ValueError(f"SKILL.md in {skill_dir} is missing a 'name' field in frontmatter")
6247
+
6248
+
6249
+ def _pick_dir_name(type_dir: Path, name: str, source_hash: str) -> str:
6250
+ """Pick a unique directory name, appending a hash suffix on collision."""
6251
+ if not (type_dir / name).exists():
6252
+ return name
6253
+ return f"{name}-{source_hash[:8]}"
6254
+
6255
+
6256
+ def _rename_and_cache_skill(type_dir: Path, temp_dir: Path, source_hash: str, skill_root: Path,
6257
+ source_label: str = "") -> Path:
6258
+ """Validate SKILL.md, rename the temp dir to the skill's declared name, and update the map.
6259
+
6260
+ Raises ValueError if SKILL.md is missing or has invalid frontmatter.
6261
+ """
6262
+ if not (skill_root / "SKILL.md").exists():
6263
+ _cleanup(temp_dir)
6264
+ hint = f" (source: {source_label})" if source_label else ""
6265
+ raise ValueError(f"No SKILL.md found in fetched skill{hint}")
6266
+
6267
+ name = _read_skill_name(skill_root)
6268
+ dir_name = _pick_dir_name(type_dir, name, source_hash)
6269
+ final_dir = type_dir / dir_name
6270
+ if final_dir != temp_dir:
6271
+ temp_dir.rename(final_dir)
6272
+
6273
+ mapping = _read_map(type_dir)
6274
+ mapping[source_hash] = dir_name
6275
+ _write_map(type_dir, mapping)
6276
+ return final_dir
6277
+
6278
+
6279
+ def _fetch_s3_skill(source: str, s3_client=None) -> Path:
6280
+ """Download an s3:// skill prefix and return the local directory."""
6281
+ uri = source if source.endswith("/") else source + "/"
6282
+ source_hash = _stable_hash(uri)
6283
+ type_dir = _SKILLS_BASE / "s3"
6284
+
6285
+ cached = _resolve_cached(type_dir, source_hash)
6286
+ if cached:
6287
+ return Path(cached)
6288
+
6289
+ import boto3
6290
+ client = s3_client or boto3.client("s3")
6291
+ bucket, _, prefix = uri[len("s3://"):].partition("/")
6292
+ if not bucket:
6293
+ raise ValueError(f"Invalid S3 URI (no bucket): {uri}")
6294
+
6295
+ temp_dir = type_dir / source_hash
6296
+ _cleanup(temp_dir)
6297
+ temp_dir.mkdir(parents=True, exist_ok=True)
6298
+ temp_root = temp_dir.resolve()
6299
+
6300
+ paginator = client.get_paginator("list_objects_v2")
6301
+ total = 0
6302
+ for page in paginator.paginate(Bucket=bucket, Prefix=prefix):
6303
+ for obj in page.get("Contents", []):
6304
+ total += obj["Size"]
6305
+ if total > _S3_MAX_SIZE_BYTES:
6306
+ _cleanup(temp_dir)
6307
+ raise ValueError(f"S3 skill {uri} exceeds 1 GB size limit")
6308
+ rel = obj["Key"][len(prefix):].lstrip("/")
6309
+ if not rel:
6310
+ continue
6311
+ dest = (temp_dir / rel).resolve()
6312
+ if dest != temp_root and not str(dest).startswith(str(temp_root) + os.sep):
6313
+ _cleanup(temp_dir)
6314
+ raise ValueError(f"Path traversal detected in S3 key: {obj['Key']}")
6315
+ dest.parent.mkdir(parents=True, exist_ok=True)
6316
+ client.download_file(bucket, obj["Key"], str(dest))
6317
+
6318
+ if total == 0:
6319
+ _cleanup(temp_dir)
6320
+ raise ValueError(f"No files found at S3 URI: {uri}")
6321
+
6322
+ return _rename_and_cache_skill(type_dir, temp_dir, source_hash, temp_dir, source_label=uri)
6323
+
6324
+
6325
+ def _resolve_credential_arn(credential_arn: str, identity_client) -> str:
6326
+ """Resolve a Token Vault API-key credential ARN to its secret value via AgentCore Identity.
6327
+
6328
+ ARN format: arn:<p>:bedrock-agentcore:<region>:<account>:token-vault/<vault>/apikeycredentialprovider/<name>
6329
+ """
6330
+ from bedrock_agentcore.runtime.context import BedrockAgentCoreContext # noqa: PLC0415
6331
+
6332
+ provider_name = credential_arn.rsplit("/", 1)[-1]
6333
+ if not provider_name:
6334
+ raise ValueError(f"Invalid credential ARN: {credential_arn}")
6335
+ workload_token = BedrockAgentCoreContext.get_workload_access_token()
6336
+ if not workload_token:
6337
+ raise ValueError("Credential ARN resolution requires a workload access token")
6338
+ api_key = identity_client.dp_client.get_resource_api_key(
6339
+ resourceCredentialProviderName=provider_name,
6340
+ workloadIdentityToken=workload_token,
6341
+ )["apiKey"]
6342
+ if not api_key:
6343
+ raise ValueError(f"Identity returned empty API key for provider: {provider_name}")
6344
+ return api_key
6345
+
6346
+
6347
+ def _build_git_auth_env(credential_arn: Optional[str], username: Optional[str], identity_client=None) -> dict:
6348
+ """Build GIT_CONFIG_* env vars for HTTP Basic auth using a Token Vault credential ARN.
6349
+
6350
+ Uses env vars instead of -c args to avoid leaking credentials in /proc/*/cmdline,
6351
+ and so auth propagates to sub-commands (e.g. sparse-checkout triggering a fetch).
6352
+ """
6353
+ if not credential_arn or not identity_client:
6354
+ return {}
6355
+ password = _resolve_credential_arn(credential_arn, identity_client)
6356
+ user = username or "oauth2"
6357
+ encoded = base64.b64encode(f"{user}:{password}".encode()).decode()
6358
+ return {
6359
+ "GIT_CONFIG_COUNT": "1",
6360
+ "GIT_CONFIG_KEY_0": "http.extraHeader",
6361
+ "GIT_CONFIG_VALUE_0": f"Authorization: Basic {encoded}",
6362
+ }
6363
+
6364
+
6365
+ def _fetch_git_skill(url: str, skill_path: str = "", credential_arn: Optional[str] = None,
6366
+ username: Optional[str] = None, identity_client=None) -> Path:
6367
+ """Shallow-clone a git skill repository and return the local skill directory.
6368
+
6369
+ Returns the directory containing SKILL.md (the subdir itself for sparse checkouts).
6370
+ """
6371
+ if skill_path and (os.path.isabs(skill_path) or ".." in Path(skill_path).parts):
6372
+ raise ValueError(f"Path traversal detected in skill path: {skill_path}")
6373
+
6374
+ source_hash = _stable_hash(f"{url}:{skill_path}")
6375
+ type_dir = _SKILLS_BASE / "git"
6376
+
6377
+ cached = _resolve_cached(type_dir, source_hash)
6378
+ if cached:
6379
+ return Path(cached) / skill_path if skill_path else Path(cached)
6380
+
6381
+ temp_dir = type_dir / source_hash
6382
+ _cleanup(temp_dir)
6383
+ temp_dir.mkdir(parents=True, exist_ok=True)
6384
+
6385
+ extra_env = _build_git_auth_env(credential_arn, username, identity_client)
6386
+ git_env = {**os.environ, **extra_env} if extra_env else None
6387
+
6388
+ try:
6389
+ if skill_path:
6390
+ subprocess.run(
6391
+ ["git", "clone", "--depth", "1", "--filter=blob:none", "--sparse", url, str(temp_dir)],
6392
+ check=True, timeout=_GIT_TIMEOUT, capture_output=True, env=git_env,
6393
+ )
6394
+ subprocess.run(
6395
+ ["git", "sparse-checkout", "set", skill_path],
6396
+ check=True, timeout=_GIT_TIMEOUT, capture_output=True, cwd=str(temp_dir), env=git_env,
6397
+ )
6398
+ else:
6399
+ subprocess.run(
6400
+ ["git", "clone", "--depth", "1", url, str(temp_dir)],
6401
+ check=True, timeout=_GIT_TIMEOUT, capture_output=True, env=git_env,
6402
+ )
6403
+ except Exception:
6404
+ _cleanup(temp_dir)
6405
+ raise
6406
+
6407
+ if skill_path and not (temp_dir / skill_path).exists():
6408
+ _cleanup(temp_dir)
6409
+ raise ValueError(f"Skill path '{skill_path}' not found in repository '{url}'")
6410
+
6411
+ # SKILL.md lives inside the subdir for sparse checkouts.
6412
+ skill_root = temp_dir / skill_path if skill_path else temp_dir
6413
+ label = f"{url}:{skill_path}" if skill_path else url
6414
+ final_dir = _rename_and_cache_skill(type_dir, temp_dir, source_hash, skill_root, source_label=label)
6415
+ return final_dir / skill_path if skill_path else final_dir
6416
+
6417
+
6418
+ def resolve_s3_skills(sources: list, s3_client=None) -> list:
6419
+ """Resolve s3:// skill URIs to local filesystem paths.
6420
+
6421
+ Any fetch failure raises and fails the invocation — a partial skill set
6422
+ would silently run the agent without capabilities the harness declared.
6423
+ """
6424
+ paths = []
6425
+ for uri in sources:
6426
+ try:
6427
+ skill_dir = _fetch_s3_skill(uri, s3_client)
6428
+ except Exception as e:
6429
+ raise ValueError(f"Failed to resolve S3 skill '{uri}': {e}") from e
6430
+ paths.append(str(skill_dir.resolve()))
6431
+ return paths
6432
+
6433
+
6434
+ def resolve_git_skills(sources: list, identity_client=None) -> list:
6435
+ """Resolve git skill dicts to local filesystem paths.
6436
+
6437
+ Each source is a dict with keys: url (required), path (optional),
6438
+ credentialArn (optional), username (optional).
6439
+
6440
+ Any fetch failure raises and fails the invocation — a partial skill set
6441
+ would silently run the agent without capabilities the harness declared.
6442
+ """
6443
+ paths = []
6444
+ for source in sources:
6445
+ try:
6446
+ skill_dir = _fetch_git_skill(
6447
+ url=source["url"],
6448
+ skill_path=source.get("path") or "",
6449
+ credential_arn=source.get("credentialArn"),
6450
+ username=source.get("username"),
6451
+ identity_client=identity_client,
6452
+ )
6453
+ except Exception as e:
6454
+ raise ValueError(f"Failed to resolve git skill '{source.get('url', source)}': {e}") from e
6455
+ paths.append(str(skill_dir.resolve()))
6456
+ return paths
6457
+ "
6458
+ `;
6459
+
6460
+ exports[`Assets Directory Snapshots > Python framework assets > python/python/http/strands/capabilities/execution-limits/hooks/execution_limits.py should match snapshot 1`] = `
6461
+ "import time
6462
+ from typing import Optional
6463
+
6464
+ from strands.hooks import BeforeModelCallEvent
6465
+ from strands.hooks.registry import HookProvider, HookRegistry
6466
+ from strands.types.exceptions import EventLoopException
6467
+
6468
+
6469
+ class ExecutionLimitExceeded(Exception):
6470
+ def __init__(self, message: str) -> None:
6471
+ super().__init__(message)
6472
+
6473
+
6474
+ class ExecutionLimitsHook(HookProvider):
6475
+ def __init__(
6476
+ self,
6477
+ max_iterations: Optional[int] = None,
6478
+ max_tokens: Optional[int] = None,
6479
+ timeout_seconds: Optional[float] = None,
6480
+ ) -> None:
6481
+ self._max_iterations = max_iterations
6482
+ self._max_tokens = max_tokens
6483
+ self._timeout_seconds = timeout_seconds
6484
+ self._iteration_count = 0
6485
+ self._start_time = time.monotonic()
6486
+
6487
+ def register_hooks(self, registry: HookRegistry, **kwargs) -> None:
6488
+ registry.add_callback(BeforeModelCallEvent, self._check_limits)
6489
+
6490
+ def _check_limits(self, event: BeforeModelCallEvent) -> None:
6491
+ self._iteration_count += 1
6492
+
6493
+ if self._max_iterations is not None and self._iteration_count > self._max_iterations:
6494
+ raise EventLoopException(
6495
+ ExecutionLimitExceeded(f"Max iterations exceeded: {self._max_iterations}")
6496
+ )
6497
+
6498
+ if self._timeout_seconds is not None:
6499
+ elapsed = time.monotonic() - self._start_time
6500
+ if elapsed > self._timeout_seconds:
6501
+ raise EventLoopException(
6502
+ ExecutionLimitExceeded(
6503
+ f"Timeout exceeded: {self._timeout_seconds}s (elapsed {elapsed:.1f}s)"
6504
+ )
6505
+ )
6506
+
6507
+ if self._max_tokens is not None:
6508
+ used = event.agent.event_loop_metrics.accumulated_usage.get("outputTokens", 0)
6509
+ if used >= self._max_tokens:
6510
+ raise EventLoopException(
6511
+ ExecutionLimitExceeded(
6512
+ f"Max output tokens exceeded: {used}/{self._max_tokens}"
6513
+ )
6514
+ )
6515
+ "
6516
+ `;
6517
+
5429
6518
  exports[`Assets Directory Snapshots > Python framework assets > python/python/http/strands/capabilities/memory/__init__.py should match snapshot 1`] = `
5430
6519
  "# Package marker
5431
6520
  "
@@ -5458,8 +6547,11 @@ def get_memory_session_manager(session_id: Optional[str], actor_id: str) -> Opti
5458
6547
  {{#if (includes memoryProviders.[0].strategies "USER_PREFERENCE")}}
5459
6548
  f"/users/{actor_id}/preferences": RetrievalConfig(top_k=3, relevance_score=0.5),
5460
6549
  {{/if}}
6550
+ {{#if (includes memoryProviders.[0].strategies "EPISODIC")}}
6551
+ f"/episodes/{actor_id}/{session_id}": RetrievalConfig(top_k=5, relevance_score=0.5),
6552
+ {{/if}}
5461
6553
  {{#if (includes memoryProviders.[0].strategies "SUMMARIZATION")}}
5462
- f"/summaries/{actor_id}": RetrievalConfig(top_k=3, relevance_score=0.5),
6554
+ f"/summaries/{actor_id}/{session_id}": RetrievalConfig(top_k=3, relevance_score=0.5),
5463
6555
  {{/if}}
5464
6556
  }
5465
6557
  {{/if}}
@@ -5479,6 +6571,160 @@ def get_memory_session_manager(session_id: Optional[str], actor_id: str) -> Opti
5479
6571
  "
5480
6572
  `;
5481
6573
 
6574
+ exports[`Assets Directory Snapshots > Python framework assets > python/python/http/strands/capabilities/payments/__init__.py should match snapshot 1`] = `
6575
+ """"Payment capabilities for Strands agents."""
6576
+ from .payments import create_payments_plugin
6577
+
6578
+ __all__ = ["create_payments_plugin"]
6579
+ "
6580
+ `;
6581
+
6582
+ exports[`Assets Directory Snapshots > Python framework assets > python/python/http/strands/capabilities/payments/payments.py should match snapshot 1`] = `
6583
+ """"Payment capability -- auto-generated by agentcore CLI.
6584
+
6585
+ Configures AgentCorePaymentsPlugin for Strands agents.
6586
+ Manager config is auto-discovered from AGENTCORE_PAYMENT_*
6587
+ environment variables set at deploy time.
6588
+
6589
+ Uses a per-invocation factory pattern so each request gets its own
6590
+ plugin instance with the correct user_id, instrument_id, and session_id.
6591
+ This prevents concurrency bugs where one user's payment context
6592
+ could leak to another user's request.
6593
+
6594
+ Uses the official SDK plugin which handles:
6595
+ - x402 v1 (body-based) and v2 (header-based) payment detection
6596
+ - Automatic 402 response interception and payment processing
6597
+ - Retry limiting (max 3 payment retries per tool use)
6598
+ - Error management and logging
6599
+ """
6600
+ import os
6601
+ import logging
6602
+
6603
+ import boto3
6604
+ from bedrock_agentcore.payments.integrations.strands import AgentCorePaymentsPlugin
6605
+ from bedrock_agentcore.payments.integrations.config import AgentCorePaymentsPluginConfig
6606
+
6607
+ logger = logging.getLogger(__name__)
6608
+
6609
+ PAYMENT_SYSTEM_PROMPT = """
6610
+ You have payment capabilities via the x402 protocol:
6611
+ - Use http_request to call HTTP endpoints. 402 Payment Required responses are settled automatically by the plugin and the call is retried.
6612
+ - Use get_payment_session to check your remaining budget before expensive operations
6613
+ - Use get_payment_instrument_balance to check wallet USDC balance
6614
+ - Use list_payment_instruments to see available payment instruments
6615
+ - If budget is low, inform the user before proceeding with paid requests
6616
+ """
6617
+
6618
+ _manager_arn = None
6619
+ _connector_id = None
6620
+ _process_payment_role_arn = None
6621
+ _name_segment = None
6622
+ _region = None
6623
+ _auth_mode = None
6624
+ _manager_count = 0
6625
+ for key, value in os.environ.items():
6626
+ if key.startswith("AGENTCORE_PAYMENT_") and key.endswith("_MANAGER_ARN"):
6627
+ if _manager_arn is None:
6628
+ _manager_arn = value
6629
+ _name_segment = key[len("AGENTCORE_PAYMENT_"):-len("_MANAGER_ARN")]
6630
+ _manager_count += 1
6631
+ if _manager_count > 1:
6632
+ logger.warning(
6633
+ "Multiple payment managers detected in environment. Using the first one found. "
6634
+ "Remove extra AGENTCORE_PAYMENT_*_MANAGER_ARN env vars to eliminate ambiguity."
6635
+ )
6636
+ _region = os.getenv("AWS_REGION")
6637
+
6638
+ _prefix = f"AGENTCORE_PAYMENT_{_name_segment}_" if _name_segment else "AGENTCORE_PAYMENT_"
6639
+ _auth_mode = os.getenv(f"{_prefix}AUTH_MODE", "sigv4")
6640
+ _connector_id = os.getenv(f"{_prefix}CONNECTOR_ID")
6641
+ _process_payment_role_arn = os.getenv(f"{_prefix}PROCESS_PAYMENT_ROLE_ARN")
6642
+ _auto_payment = os.getenv(f"{_prefix}AUTO_PAYMENT", "true").lower() == "true"
6643
+ _allowlist_raw = os.getenv(f"{_prefix}TOOL_ALLOWLIST")
6644
+ _allowlist = _allowlist_raw.split(",") if _allowlist_raw else None
6645
+ _network_prefs_raw = os.getenv(f"{_prefix}NETWORK_PREFERENCES")
6646
+ _network_prefs = _network_prefs_raw.split(",") if _network_prefs_raw else None
6647
+
6648
+ if not _manager_arn:
6649
+ logger.warning("No payment manager config found in environment")
6650
+ if not _connector_id:
6651
+ logger.warning("No payment connector config found in environment")
6652
+
6653
+
6654
+ def _assume_role_session(role_arn):
6655
+ """Assume an IAM role and return a boto3 session with temporary credentials."""
6656
+ sts = boto3.client("sts", region_name=_region)
6657
+ creds = sts.assume_role(
6658
+ RoleArn=role_arn,
6659
+ RoleSessionName="agentcore-payment-plugin",
6660
+ )["Credentials"]
6661
+ return boto3.Session(
6662
+ aws_access_key_id=creds["AccessKeyId"],
6663
+ aws_secret_access_key=creds["SecretAccessKey"],
6664
+ aws_session_token=creds["SessionToken"],
6665
+ region_name=_region,
6666
+ )
6667
+
6668
+
6669
+ def create_payments_plugin(user_id, instrument_id=None, session_id=None):
6670
+ """Create a fresh plugin instance per invocation.
6671
+
6672
+ Args:
6673
+ user_id: From invocation context (required for SigV4, derived from JWT for bearer)
6674
+ instrument_id: From invocation payload (created by app backend per user)
6675
+ session_id: From invocation payload (created by app backend per conversation)
6676
+
6677
+ Returns:
6678
+ AgentCorePaymentsPlugin instance, or None if no manager is configured.
6679
+ """
6680
+ if not _manager_arn:
6681
+ return None
6682
+
6683
+ config_kwargs = {
6684
+ "payment_manager_arn": _manager_arn,
6685
+ "region": _region,
6686
+ "payment_instrument_id": instrument_id,
6687
+ "payment_session_id": session_id,
6688
+ "payment_connector_id": _connector_id,
6689
+ }
6690
+
6691
+ config_kwargs["auto_payment"] = _auto_payment
6692
+ if _allowlist:
6693
+ config_kwargs["payment_tool_allowlist"] = _allowlist
6694
+ if _network_prefs:
6695
+ config_kwargs["network_preferences_config"] = _network_prefs
6696
+
6697
+ if _process_payment_role_arn:
6698
+ # Only pass boto3_session if SDK supports it (added in bedrock-agentcore >= 1.11).
6699
+ # Older SDKs use the runtime role's default credentials and can still call ProcessPayment
6700
+ # if the runtime role has been granted permission directly.
6701
+ import inspect
6702
+ if "boto3_session" in inspect.signature(AgentCorePaymentsPluginConfig).parameters:
6703
+ config_kwargs["boto3_session"] = _assume_role_session(_process_payment_role_arn)
6704
+ else:
6705
+ logger.warning(
6706
+ "PROCESS_PAYMENT_ROLE_ARN set but bedrock-agentcore SDK does not support boto3_session. "
6707
+ "Upgrade to bedrock-agentcore>=1.11 to enable cross-role payment processing."
6708
+ )
6709
+
6710
+ if _auth_mode == "bearer":
6711
+ bearer_token = os.getenv("AGENTCORE_BEARER_TOKEN")
6712
+ if bearer_token:
6713
+ config_kwargs["bearer_token"] = bearer_token
6714
+ else:
6715
+ logger.warning(
6716
+ "Bearer auth mode configured but AGENTCORE_BEARER_TOKEN not set. "
6717
+ "Falling back to SigV4. Set AGENTCORE_BEARER_TOKEN or pass bearer_token in invoke context."
6718
+ )
6719
+ config_kwargs["user_id"] = user_id or "default-user"
6720
+ else:
6721
+ config_kwargs["user_id"] = user_id or "default-user"
6722
+
6723
+ config = AgentCorePaymentsPluginConfig(**config_kwargs)
6724
+ return AgentCorePaymentsPlugin(config=config)
6725
+ "
6726
+ `;
6727
+
5482
6728
  exports[`Assets Directory Snapshots > Python framework assets > python/python/mcp/standalone/base/README.md should match snapshot 1`] = `
5483
6729
  "# {{ name }}
5484
6730
 
@@ -5968,6 +7214,19 @@ When modifying JSON config files:
5968
7214
  4. Use CloudFormation-safe names (alphanumeric, start with letter)
5969
7215
  5. Run \`agentcore validate\` to verify changes
5970
7216
 
7217
+ ## Harness Export
7218
+
7219
+ \`agentcore export harness\` converts a harness configuration into a deployable Strands Python agent under \`app/<agentName>/\`.
7220
+
7221
+ **After every export, you MUST read \`app/<agentName>/EXPORT_NOTES.md\` before proceeding.**
7222
+
7223
+ 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\`.
7224
+
7225
+ \`\`\`bash
7226
+ agentcore export harness --name <harnessName> # generates app/<agentName>/EXPORT_NOTES.md
7227
+ cat app/<agentName>/EXPORT_NOTES.md # read this before touching anything else
7228
+ \`\`\`
7229
+
5971
7230
  ## CLI Commands
5972
7231
 
5973
7232
  | Command | Description |
@@ -5975,6 +7234,7 @@ When modifying JSON config files:
5975
7234
  | \`agentcore create\` | Create a new project |
5976
7235
  | \`agentcore add <resource>\` | Add agent, memory, credential, gateway, evaluator, policy |
5977
7236
  | \`agentcore remove <resource>\` | Remove a resource |
7237
+ | \`agentcore export harness\` | Export a harness to a Strands runtime agent |
5978
7238
  | \`agentcore dev\` | Run agent locally with hot-reload |
5979
7239
  | \`agentcore deploy\` | Deploy to AWS |
5980
7240
  | \`agentcore status\` | Show deployment status |