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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (91) hide show
  1. package/README.md +31 -17
  2. package/dist/agent-inspector/index.css +1 -1
  3. package/dist/agent-inspector/index.js +90 -85
  4. package/dist/assets/__tests__/__snapshots__/assets.snapshot.test.ts.snap +966 -106
  5. package/dist/assets/agents/AGENTS.md +14 -0
  6. package/dist/assets/cdk/bin/cdk.ts +42 -20
  7. package/dist/assets/cdk/lib/cdk-stack.ts +19 -14
  8. package/dist/assets/cdk/test/cdk.test.ts +2 -1
  9. package/dist/assets/container/python/Dockerfile +4 -0
  10. package/dist/assets/python/a2a/strands/capabilities/memory/session.py +4 -1
  11. package/dist/assets/python/agui/strands/capabilities/memory/session.py +4 -1
  12. package/dist/assets/python/http/googleadk/base/mcp_client/client.py +7 -7
  13. package/dist/assets/python/http/googleadk/base/pyproject.toml +1 -1
  14. package/dist/assets/python/http/langchain_langgraph/base/mcp_client/client.py +10 -8
  15. package/dist/assets/python/http/langchain_langgraph/base/pyproject.toml +1 -1
  16. package/dist/assets/python/http/openaiagents/base/main.py +19 -9
  17. package/dist/assets/python/http/openaiagents/base/mcp_client/client.py +4 -4
  18. package/dist/assets/python/http/openaiagents/base/pyproject.toml +1 -1
  19. package/dist/assets/python/http/strands/base/main.py +438 -27
  20. package/dist/assets/python/http/strands/base/mcp_client/client.py +45 -5
  21. package/dist/assets/python/http/strands/base/model/load.py +3 -3
  22. package/dist/assets/python/http/strands/base/pyproject.toml +5 -2
  23. package/dist/assets/python/http/strands/base/skills/fetcher.py +279 -0
  24. package/dist/assets/python/http/strands/capabilities/execution-limits/hooks/execution_limits.py +54 -0
  25. package/dist/assets/python/http/strands/capabilities/memory/session.py +4 -1
  26. package/dist/cli/index.mjs +1274 -1088
  27. package/dist/lib/errors/types.d.ts +18 -0
  28. package/dist/lib/errors/types.d.ts.map +1 -1
  29. package/dist/lib/errors/types.js +28 -1
  30. package/dist/lib/errors/types.js.map +1 -1
  31. package/dist/lib/schemas/io/config-io.d.ts.map +1 -1
  32. package/dist/lib/schemas/io/config-io.js +0 -2
  33. package/dist/lib/schemas/io/config-io.js.map +1 -1
  34. package/dist/schema/constants.d.ts +20 -0
  35. package/dist/schema/constants.d.ts.map +1 -1
  36. package/dist/schema/constants.js +25 -1
  37. package/dist/schema/constants.js.map +1 -1
  38. package/dist/schema/schemas/agent-env.d.ts +35 -0
  39. package/dist/schema/schemas/agent-env.d.ts.map +1 -1
  40. package/dist/schema/schemas/agentcore-project.d.ts +184 -25
  41. package/dist/schema/schemas/agentcore-project.d.ts.map +1 -1
  42. package/dist/schema/schemas/agentcore-project.js +81 -31
  43. package/dist/schema/schemas/agentcore-project.js.map +1 -1
  44. package/dist/schema/schemas/auth.d.ts +175 -0
  45. package/dist/schema/schemas/auth.d.ts.map +1 -1
  46. package/dist/schema/schemas/auth.js +126 -3
  47. package/dist/schema/schemas/auth.js.map +1 -1
  48. package/dist/schema/schemas/deployed-state.d.ts +140 -46
  49. package/dist/schema/schemas/deployed-state.d.ts.map +1 -1
  50. package/dist/schema/schemas/deployed-state.js +39 -14
  51. package/dist/schema/schemas/deployed-state.js.map +1 -1
  52. package/dist/schema/schemas/mcp-defs.d.ts +2 -1
  53. package/dist/schema/schemas/mcp-defs.d.ts.map +1 -1
  54. package/dist/schema/schemas/mcp-defs.js +3 -2
  55. package/dist/schema/schemas/mcp-defs.js.map +1 -1
  56. package/dist/schema/schemas/mcp.d.ts +186 -0
  57. package/dist/schema/schemas/mcp.d.ts.map +1 -1
  58. package/dist/schema/schemas/mcp.js +479 -4
  59. package/dist/schema/schemas/mcp.js.map +1 -1
  60. package/dist/schema/schemas/primitives/ab-test.d.ts +0 -6
  61. package/dist/schema/schemas/primitives/ab-test.d.ts.map +1 -1
  62. package/dist/schema/schemas/primitives/ab-test.js +0 -2
  63. package/dist/schema/schemas/primitives/ab-test.js.map +1 -1
  64. package/dist/schema/schemas/primitives/harness.d.ts +209 -11
  65. package/dist/schema/schemas/primitives/harness.d.ts.map +1 -1
  66. package/dist/schema/schemas/primitives/harness.js +268 -21
  67. package/dist/schema/schemas/primitives/harness.js.map +1 -1
  68. package/dist/schema/schemas/primitives/index.d.ts +6 -8
  69. package/dist/schema/schemas/primitives/index.d.ts.map +1 -1
  70. package/dist/schema/schemas/primitives/index.js +6 -5
  71. package/dist/schema/schemas/primitives/index.js.map +1 -1
  72. package/dist/schema/schemas/primitives/knowledge-base.d.ts +104 -0
  73. package/dist/schema/schemas/primitives/knowledge-base.d.ts.map +1 -0
  74. package/dist/schema/schemas/primitives/knowledge-base.js +99 -0
  75. package/dist/schema/schemas/primitives/knowledge-base.js.map +1 -0
  76. package/dist/schema/schemas/primitives/online-eval-config.d.ts +20 -2
  77. package/dist/schema/schemas/primitives/online-eval-config.d.ts.map +1 -1
  78. package/dist/schema/schemas/primitives/online-eval-config.js +43 -5
  79. package/dist/schema/schemas/primitives/online-eval-config.js.map +1 -1
  80. package/dist/schema/schemas/primitives/policy.d.ts +26 -0
  81. package/dist/schema/schemas/primitives/policy.d.ts.map +1 -1
  82. package/dist/schema/schemas/primitives/policy.js +5 -1
  83. package/dist/schema/schemas/primitives/policy.js.map +1 -1
  84. package/npm-shrinkwrap.json +195 -330
  85. package/package.json +10 -8
  86. package/scripts/bundle.mjs +51 -0
  87. package/scripts/run-e2e-local.sh +40 -8
  88. package/dist/schema/schemas/primitives/http-gateway.d.ts +0 -21
  89. package/dist/schema/schemas/primitives/http-gateway.d.ts.map +0 -1
  90. package/dist/schema/schemas/primitives/http-gateway.js +0 -34
  91. package/dist/schema/schemas/primitives/http-gateway.js.map +0 -1
@@ -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';
@@ -101,40 +101,61 @@ async function main() {
101
101
  throw new Error('No deployment targets configured. Please define targets in agentcore/aws-targets.json');
102
102
  }
103
103
 
104
- // 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.
105
106
  const projectRoot = path.resolve(configRoot, '..');
106
- const harnessConfigs: {
107
- name: string;
108
- executionRoleArn?: string;
109
- memoryName?: string;
110
- containerUri?: string;
111
- hasDockerfile?: boolean;
112
- dockerfile?: string;
113
- codeLocation?: string;
114
- tools?: { type: string; name: string }[];
115
- apiKeyArn?: string;
116
- efsAccessPoints?: { accessPointArn: string; mountPath: string }[];
117
- s3AccessPoints?: { accessPointArn: string; mountPath: string }[];
118
- apiFormat?: 'converse_stream' | 'responses' | 'chat_completions';
119
- }[] = [];
120
- 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 ?? []) : []) {
121
136
  const harnessDir = path.resolve(projectRoot, entry.path);
122
137
  const harnessPath = path.resolve(harnessDir, 'harness.json');
123
138
  try {
124
- const harnessSpec = JSON.parse(fs.readFileSync(harnessPath, 'utf-8'));
139
+ const harnessSpec = HarnessSpecSchema.parse(JSON.parse(fs.readFileSync(harnessPath, 'utf-8')));
125
140
  harnessConfigs.push({
126
141
  name: entry.name,
127
142
  executionRoleArn: harnessSpec.executionRoleArn,
128
- 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,
129
146
  containerUri: harnessSpec.containerUri,
130
147
  hasDockerfile: !!harnessSpec.dockerfile,
131
148
  dockerfile: harnessSpec.dockerfile,
132
149
  codeLocation: harnessSpec.dockerfile ? harnessDir : undefined,
133
150
  tools: harnessSpec.tools,
151
+ skills: harnessSpec.skills,
134
152
  apiKeyArn: harnessSpec.model?.apiKeyArn,
135
153
  efsAccessPoints: harnessSpec.efsAccessPoints,
136
154
  s3AccessPoints: harnessSpec.s3AccessPoints,
137
155
  apiFormat: harnessSpec.model?.apiFormat,
156
+ // Full spec + dir drive the AWS::BedrockAgentCore::Harness CFN resource.
157
+ spec: harnessSpec,
158
+ harnessDir,
138
159
  });
139
160
  } catch (err) {
140
161
  throw new Error(
@@ -201,6 +222,7 @@ async function main() {
201
222
  spec,
202
223
  mcpSpec,
203
224
  credentials,
225
+ connectorParametersByFile,
204
226
  harnesses: harnessConfigs.length > 0 ? harnessConfigs : undefined,
205
227
  paymentSpec,
206
228
  env,
@@ -349,24 +371,18 @@ exports[`Assets Directory Snapshots > CDK assets > cdk/cdk/lib/cdk-stack.ts shou
349
371
  type AgentCoreProjectSpec,
350
372
  type AgentCoreMcpSpec,
351
373
  type CustomJWTAuthorizerConfig,
374
+ type HarnessDeploymentConfig,
352
375
  } from '@aws/agentcore-cdk';
353
376
  import { CfnOutput, Stack, type StackProps } from 'aws-cdk-lib';
354
377
  import * as iam from 'aws-cdk-lib/aws-iam';
355
378
  import { Construct } from 'constructs';
356
379
 
357
- export interface HarnessConfig {
358
- name: string;
359
- executionRoleArn?: string;
360
- memoryName?: string;
361
- containerUri?: string;
362
- hasDockerfile?: boolean;
363
- dockerfile?: string;
364
- codeLocation?: string;
365
- tools?: { type: string; name: string }[];
366
- apiKeyArn?: string;
367
- efsAccessPoints?: { accessPointArn: string; mountPath: string }[];
368
- s3AccessPoints?: { accessPointArn: string; mountPath: string }[];
369
- }
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;
370
386
 
371
387
  export interface PaymentConnectorSpec {
372
388
  name: string;
@@ -402,6 +418,11 @@ export interface AgentCoreStackProps extends StackProps {
402
418
  * Harness role configurations.
403
419
  */
404
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>>;
405
426
  /**
406
427
  * Payment specifications with resolved credential provider ARNs.
407
428
  */
@@ -441,7 +462,7 @@ export class AgentCoreStack extends Stack {
441
462
  constructor(scope: Construct, id: string, props: AgentCoreStackProps) {
442
463
  super(scope, id, props);
443
464
 
444
- const { spec, mcpSpec, credentials, harnesses, paymentSpec } = props;
465
+ const { spec, mcpSpec, credentials, harnesses, connectorParametersByFile, paymentSpec } = props;
445
466
 
446
467
  // Create AgentCoreApplication with all agents and harness roles
447
468
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
@@ -449,6 +470,12 @@ export class AgentCoreStack extends Stack {
449
470
  if (harnesses?.length) {
450
471
  appProps.harnesses = harnesses;
451
472
  }
473
+ if (connectorParametersByFile && Object.keys(connectorParametersByFile).length > 0) {
474
+ appProps.connectorParametersByFile = connectorParametersByFile;
475
+ }
476
+ if (credentials) {
477
+ appProps.credentials = credentials;
478
+ }
452
479
  this.application = new AgentCoreApplication(this, 'Application', appProps as any);
453
480
 
454
481
  // Create AgentCoreMcp if there are gateways configured
@@ -649,13 +676,14 @@ test('AgentCoreStack synthesizes with empty spec', () => {
649
676
  credentials: [],
650
677
  evaluators: [],
651
678
  onlineEvalConfigs: [],
679
+ configBundles: [],
652
680
  policyEngines: [],
653
681
  payments: [],
654
- configBundles: [],
655
682
  agentCoreGateways: [],
656
683
  mcpRuntimeTools: [],
657
684
  unassignedTargets: [],
658
685
  datasets: [],
686
+ knowledgeBases: [],
659
687
  },
660
688
  });
661
689
  const template = Template.fromStack(stack);
@@ -810,6 +838,8 @@ exports[`Assets Directory Snapshots > File listing > should match the expected f
810
838
  "python/http/strands/base/model/__init__.py",
811
839
  "python/http/strands/base/model/load.py",
812
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",
813
843
  "python/http/strands/capabilities/memory/__init__.py",
814
844
  "python/http/strands/capabilities/memory/session.py",
815
845
  "python/http/strands/capabilities/payments/__init__.py",
@@ -2227,8 +2257,11 @@ def get_memory_session_manager(session_id: Optional[str], actor_id: str) -> Opti
2227
2257
  {{#if (includes memoryProviders.[0].strategies "USER_PREFERENCE")}}
2228
2258
  f"/users/{actor_id}/preferences": RetrievalConfig(top_k=3, relevance_score=0.5),
2229
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}}
2230
2263
  {{#if (includes memoryProviders.[0].strategies "SUMMARIZATION")}}
2231
- 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),
2232
2265
  {{/if}}
2233
2266
  }
2234
2267
  {{/if}}
@@ -3246,8 +3279,11 @@ def get_memory_session_manager(session_id: Optional[str], actor_id: str) -> Opti
3246
3279
  {{#if (includes memoryProviders.[0].strategies "USER_PREFERENCE")}}
3247
3280
  f"/users/{actor_id}/preferences": RetrievalConfig(top_k=3, relevance_score=0.5),
3248
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}}
3249
3285
  {{#if (includes memoryProviders.[0].strategies "SUMMARIZATION")}}
3250
- 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),
3251
3287
  {{/if}}
3252
3288
  }
3253
3289
  {{/if}}
@@ -4000,20 +4036,20 @@ def get_all_gateway_mcp_toolsets() -> list[MCPToolset]:
4000
4036
  {{#each gatewayProviders}}
4001
4037
  url = os.environ.get("{{envVarName}}")
4002
4038
  if url:
4003
- {{#if (eq authType "AWS_IAM")}}
4039
+ {{#if (eq authType "AWS_IAM")}}
4004
4040
  session = create_aws_session()
4005
4041
  auth = SigV4HTTPXAuth(session.get_credentials(), "bedrock-agentcore", session.region_name)
4006
- toolsets.append(MCPToolset(connection_params=StreamableHTTPConnectionParams(
4042
+ toolsets.append(MCPToolset(tool_name_prefix="{{snakeCase name}}", connection_params=StreamableHTTPConnectionParams(
4007
4043
  url=url,
4008
4044
  httpx_client_factory=lambda **kwargs: httpx.AsyncClient(auth=auth, **kwargs)
4009
4045
  )))
4010
- {{else if (eq authType "CUSTOM_JWT")}}
4046
+ {{else if (eq authType "CUSTOM_JWT")}}
4011
4047
  token = _get_bearer_token_{{snakeCase name}}()
4012
4048
  headers = {"Authorization": f"Bearer {token}"} if token else None
4013
- toolsets.append(MCPToolset(connection_params=StreamableHTTPConnectionParams(url=url, headers=headers)))
4014
- {{else}}
4015
- toolsets.append(MCPToolset(connection_params=StreamableHTTPConnectionParams(url=url)))
4016
- {{/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}}
4017
4053
  else:
4018
4054
  logger.warning("{{envVarName}} not set — {{name}} gateway tools unavailable")
4019
4055
  {{/each}}
@@ -4106,7 +4142,7 @@ requires-python = ">=3.10"
4106
4142
  dependencies = [
4107
4143
  "opentelemetry-distro",
4108
4144
  "opentelemetry-exporter-otlp",
4109
- "google-adk >= 1.17.0, < 2.0.0",
4145
+ "google-adk >= 1.35.0, < 2.0.0",
4110
4146
  "google-genai >= 1.0.0, < 2.0.0",
4111
4147
  "bedrock-agentcore >= 1.0.3",
4112
4148
  "botocore[crt] >= 1.35.0",
@@ -4435,23 +4471,25 @@ def get_all_gateway_mcp_client() -> MultiServerMCPClient | None:
4435
4471
  {{#each gatewayProviders}}
4436
4472
  url = os.environ.get("{{envVarName}}")
4437
4473
  if url:
4438
- {{#if (eq authType "AWS_IAM")}}
4474
+ {{#if (eq authType "AWS_IAM")}}
4439
4475
  session = create_aws_session()
4440
4476
  auth = SigV4HTTPXAuth(session.get_credentials(), "bedrock-agentcore", session.region_name)
4441
- servers["{{name}}"] = {"transport": "streamable_http", "url": url, "auth": auth}
4442
- {{else if (eq authType "CUSTOM_JWT")}}
4477
+ servers["{{snakeCase name}}"] = {"transport": "streamable_http", "url": url, "auth": auth}
4478
+ {{else if (eq authType "CUSTOM_JWT")}}
4443
4479
  token = _get_bearer_token_{{snakeCase name}}()
4444
4480
  headers = {"Authorization": f"Bearer {token}"} if token else None
4445
- servers["{{name}}"] = {"transport": "streamable_http", "url": url, "headers": headers}
4446
- {{else}}
4447
- servers["{{name}}"] = {"transport": "streamable_http", "url": url}
4448
- {{/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}}
4449
4485
  else:
4450
4486
  logger.warning("{{envVarName}} not set — {{name}} gateway tools unavailable")
4451
4487
  {{/each}}
4452
4488
  if not servers:
4453
4489
  return None
4454
- 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)
4455
4493
  {{else}}
4456
4494
  {{#if isVpc}}
4457
4495
  # VPC mode: external MCP endpoints are not reachable without a NAT gateway.
@@ -4629,7 +4667,7 @@ dependencies = [
4629
4667
  "opentelemetry-instrumentation-langchain >= 0.59.0",
4630
4668
  "langgraph >= 1.0.2",
4631
4669
  "mcp >= 1.19.0",
4632
- "langchain-mcp-adapters >= 0.1.11",
4670
+ "langchain-mcp-adapters >= 0.2.0",
4633
4671
  "langchain >= 1.0.3",
4634
4672
  "bedrock-agentcore >= 1.8.0",
4635
4673
  "botocore[crt] >= 1.35.0",
@@ -4747,6 +4785,9 @@ exports[`Assets Directory Snapshots > Python framework assets > python/python/ht
4747
4785
  "{{#if needsOs}}
4748
4786
  import os
4749
4787
  {{/if}}
4788
+ {{#if hasGateway}}
4789
+ from contextlib import AsyncExitStack
4790
+ {{/if}}
4750
4791
  from agents import Agent, Runner, function_tool
4751
4792
  from bedrock_agentcore.runtime import BedrockAgentCoreApp
4752
4793
  from model.load import load_model
@@ -4857,15 +4898,22 @@ async def main(query):
4857
4898
  try:
4858
4899
  {{#if hasGateway}}
4859
4900
  if mcp_servers:
4860
- agent = Agent(
4861
- name="{{ name }}",
4862
- model="gpt-4.1",
4863
- instructions=INSTRUCTIONS,
4864
- mcp_servers=mcp_servers,
4865
- tools=tools
4866
- )
4867
- result = await Runner.run(agent, query)
4868
- 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
4869
4917
  else:
4870
4918
  agent = Agent(
4871
4919
  name="{{ name }}",
@@ -4965,20 +5013,20 @@ def get_all_gateway_mcp_servers() -> list[MCPServerStreamableHttp]:
4965
5013
  {{#each gatewayProviders}}
4966
5014
  url = os.environ.get("{{envVarName}}")
4967
5015
  if url:
4968
- {{#if (eq authType "AWS_IAM")}}
5016
+ {{#if (eq authType "AWS_IAM")}}
4969
5017
  session = create_aws_session()
4970
5018
  auth = SigV4HTTPXAuth(session.get_credentials(), "bedrock-agentcore", session.region_name)
4971
5019
  servers.append(MCPServerStreamableHttp(
4972
5020
  name="{{name}}",
4973
5021
  params={"url": url, "httpx_client_factory": lambda **kwargs: httpx.AsyncClient(auth=auth, **kwargs)}
4974
5022
  ))
4975
- {{else if (eq authType "CUSTOM_JWT")}}
5023
+ {{else if (eq authType "CUSTOM_JWT")}}
4976
5024
  token = _get_bearer_token_{{snakeCase name}}()
4977
5025
  headers = {"Authorization": f"Bearer {token}"} if token else {}
4978
5026
  servers.append(MCPServerStreamableHttp(name="{{name}}", params={"url": url, "headers": headers}))
4979
- {{else}}
5027
+ {{else}}
4980
5028
  servers.append(MCPServerStreamableHttp(name="{{name}}", params={"url": url}))
4981
- {{/if}}
5029
+ {{/if}}
4982
5030
  else:
4983
5031
  logger.warning("{{envVarName}} not set — {{name}} gateway tools unavailable")
4984
5032
  {{/each}}
@@ -5066,7 +5114,7 @@ readme = "README.md"
5066
5114
  requires-python = ">=3.10"
5067
5115
  dependencies = [
5068
5116
  "aws-opentelemetry-distro",
5069
- "openai-agents >= 0.4.2",
5117
+ "openai-agents >= 0.16.0",
5070
5118
  "bedrock-agentcore >= 1.8.0",
5071
5119
  "botocore[crt] >= 1.35.0",
5072
5120
  {{#if hasGateway}}{{#if (includes gatewayAuthTypes "AWS_IAM")}}"mcp-proxy-for-aws >= 1.1.0",
@@ -5167,25 +5215,77 @@ Thumbs.db"
5167
5215
 
5168
5216
  exports[`Assets Directory Snapshots > Python framework assets > python/python/http/strands/base/main.py should match snapshot 1`] = `
5169
5217
  "from typing import Any
5218
+ {{#if inlineFunctionTools}}
5219
+ import json
5170
5220
 
5221
+ from strands.tools.tools import PythonAgentTool
5222
+ from strands.types.tools import ToolResult, ToolUse
5223
+ {{/if}}
5171
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}}
5172
5246
  {{#if hasConfigBundle}}
5173
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}}
5174
5260
  from bedrock_agentcore.runtime.context import BedrockAgentCoreContext
5175
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}}
5176
5268
  from bedrock_agentcore.runtime import BedrockAgentCoreApp
5177
5269
  from model.load import load_model
5178
5270
  {{#if hasGateway}}
5179
5271
  from mcp_client.client import get_all_gateway_mcp_clients
5180
- {{else}}
5181
- from mcp_client.client import get_streamable_http_mcp_client
5182
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}}
5183
5281
  {{#if hasMemory}}
5184
5282
  from memory.session import get_memory_session_manager
5185
5283
  {{/if}}
5186
- {{#if needsOs}}
5284
+ {{#unless hasFileOperations}}
5285
+ {{#if (or needsOs (some gitSkills "credentialArn"))}}
5187
5286
  import os
5188
5287
  {{/if}}
5288
+ {{/unless}}
5189
5289
  {{#if hasPayment}}
5190
5290
  from capabilities.payments.payments import create_payments_plugin, PAYMENT_SYSTEM_PROMPT
5191
5291
  {{/if}}
@@ -5193,22 +5293,35 @@ from capabilities.payments.payments import create_payments_plugin, PAYMENT_SYSTE
5193
5293
  app = BedrockAgentCoreApp()
5194
5294
  log = app.logger
5195
5295
 
5196
- # 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 = []
5197
5299
  {{#if hasGateway}}
5198
- 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}}
5199
5305
  {{else}}
5306
+ {{#unless isExportHarness}}
5307
+ # Define a Streamable HTTP MCP Client
5200
5308
  mcp_clients = [get_streamable_http_mcp_client()]
5309
+ {{/unless}}
5201
5310
  {{/if}}
5202
5311
 
5312
+ {{#if systemPromptText}}
5313
+ DEFAULT_SYSTEM_PROMPT = """{{escapePyStr systemPromptText}}"""
5314
+ {{else}}
5203
5315
  DEFAULT_SYSTEM_PROMPT = """
5204
5316
  You are a helpful assistant. Use tools when appropriate.
5205
- {{#if needsOs}}
5317
+ {{#if needsOs}}{{#unless isExportHarness}}
5206
5318
  You have access to the following mounted filesystems. Use file_read, file_write, and list_files with full absolute paths:
5207
5319
  {{#if sessionStorageMountPath}}- {{sessionStorageMountPath}}: ephemeral session storage (lost when session ends)
5208
5320
  {{/if}}{{#each efsMounts}}- {{mountPath}}: EFS persistent storage (persists across sessions and agent restarts)
5209
5321
  {{/each}}{{#each s3Mounts}}- {{mountPath}}: S3 Files persistent storage (durable, backed by S3)
5210
- {{/each}}{{/if}}
5322
+ {{/each}}{{/unless}}{{/if}}
5211
5323
  """
5324
+ {{/if}}
5212
5325
 
5213
5326
  {{#if hasConfigBundle}}
5214
5327
  DEFAULT_TOOL_DESC = "Return the sum of two numbers"
@@ -5217,6 +5330,30 @@ DEFAULT_TOOL_DESC = "Return the sum of two numbers"
5217
5330
  # Define a collection of tools used by the model
5218
5331
  tools = []
5219
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}}
5220
5357
  # Define a simple function tool
5221
5358
  {{#if hasConfigBundle}}
5222
5359
  @tool(description=DEFAULT_TOOL_DESC)
@@ -5228,7 +5365,116 @@ def add_numbers(a: int, b: int) -> int:
5228
5365
  return a+b
5229
5366
  tools.append(add_numbers)
5230
5367
 
5231
- {{#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}}
5232
5478
  _MOUNT_PATHS = [
5233
5479
  {{#if sessionStorageMountPath}}"{{sessionStorageMountPath}}",{{/if}}
5234
5480
  {{#each efsMounts}}"{{mountPath}}",{{/each}}
@@ -5282,12 +5528,21 @@ def list_files(path: str) -> str:
5282
5528
  return f"Error listing '{path}': {e.strerror}"
5283
5529
 
5284
5530
  tools.extend([file_read, file_write, list_files])
5285
- {{/if}}
5531
+ {{/unless}}{{/if}}
5286
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}}
5287
5540
  # Add MCP client to tools if available
5288
5541
  for mcp_client in mcp_clients:
5289
5542
  if mcp_client:
5290
5543
  tools.append(mcp_client)
5544
+ {{/unless}}
5545
+ {{/if}}
5291
5546
 
5292
5547
  {{#if hasConfigBundle}}
5293
5548
 
@@ -5323,19 +5578,62 @@ class ConfigBundleHook(HookProvider):
5323
5578
 
5324
5579
  {{/if}}
5325
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
+
5326
5600
  {{#if hasMemory}}
5327
5601
  {{#unless hasPayment}}
5328
5602
  def agent_factory():
5329
5603
  cache = {}
5330
- def get_or_create_agent(session_id, user_id):
5331
- 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}"
5332
5611
  if key not in cache:
5333
5612
  cache[key] = Agent(
5334
5613
  model=load_model(),
5335
- 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(),
5336
5616
  system_prompt=DEFAULT_SYSTEM_PROMPT,
5337
- tools=tools{{#if hasConfigBundle}},
5338
- 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
+ ],
5339
5637
  )
5340
5638
  return cache[key]
5341
5639
  return get_or_create_agent
@@ -5343,24 +5641,45 @@ get_or_create_agent = agent_factory()
5343
5641
  {{/unless}}
5344
5642
  {{else}}
5345
5643
  {{#if hasConfigBundle}}
5346
- def create_agent():
5644
+ def create_agent({{#if hasSkillsFetcher}}skill_plugins=None{{/if}}):
5347
5645
  return Agent(
5348
5646
  model=load_model(),
5349
5647
  system_prompt=DEFAULT_SYSTEM_PROMPT,
5350
5648
  tools=tools,
5649
+ conversation_manager=_make_conversation_manager(),
5650
+ {{#if hasSkillsFetcher}}
5651
+ plugins=skill_plugins or None,
5652
+ {{/if}}
5351
5653
  hooks=[ConfigBundleHook()],
5352
5654
  )
5353
5655
  {{else}}
5354
5656
  {{#unless hasPayment}}
5355
5657
  _agent = None
5356
5658
 
5357
- def get_or_create_agent():
5659
+ def get_or_create_agent({{#if hasSkillsFetcher}}skill_plugins=None{{/if}}):
5358
5660
  global _agent
5359
5661
  if _agent is None:
5360
5662
  _agent = Agent(
5361
5663
  model=load_model(),
5362
5664
  system_prompt=DEFAULT_SYSTEM_PROMPT,
5363
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
+ ],
5364
5683
  )
5365
5684
  return _agent
5366
5685
  {{/unless}}
@@ -5368,6 +5687,42 @@ def get_or_create_agent():
5368
5687
  {{/if}}
5369
5688
 
5370
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
+
5371
5726
  @app.entrypoint
5372
5727
  async def invoke(payload, context):
5373
5728
  log.info("Invoking Agent.....")
@@ -5379,23 +5734,52 @@ async def invoke(payload, context):
5379
5734
  payments_plugin = create_payments_plugin(user_id, instrument_id, session_id)
5380
5735
  plugins = [payments_plugin] if payments_plugin else []
5381
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}}
5382
5758
 
5383
5759
  {{#if hasMemory}}
5384
5760
  {{#if hasPayment}}
5385
5761
  mem_session_id = getattr(context, 'session_id', 'default-session')
5762
+ {{#if actorId}}
5763
+ mem_user_id = "{{actorId}}"
5764
+ {{else}}
5386
5765
  mem_user_id = getattr(context, 'user_id', 'default-user')
5766
+ {{/if}}
5387
5767
  agent = Agent(
5388
5768
  model=load_model(),
5389
5769
  session_manager=get_memory_session_manager(mem_session_id, mem_user_id),
5390
5770
  system_prompt=DEFAULT_SYSTEM_PROMPT + PAYMENT_SYSTEM_PROMPT,
5391
5771
  tools=tools,
5392
- plugins=plugins,{{#if hasConfigBundle}}
5772
+ plugins=plugins{{#if hasSkillsFetcher}} + _skill_plugins{{/if}},{{#if hasConfigBundle}}
5393
5773
  hooks=[ConfigBundleHook()],{{/if}}
5394
5774
  )
5395
5775
  {{else}}
5396
5776
  session_id = getattr(context, 'session_id', 'default-session')
5777
+ {{#if actorId}}
5778
+ user_id = "{{actorId}}"
5779
+ {{else}}
5397
5780
  user_id = getattr(context, 'user_id', 'default-user')
5398
- 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}})
5399
5783
  {{/if}}
5400
5784
  {{else}}
5401
5785
  {{#if hasPayment}}
@@ -5403,25 +5787,100 @@ async def invoke(payload, context):
5403
5787
  model=load_model(),
5404
5788
  system_prompt=DEFAULT_SYSTEM_PROMPT + PAYMENT_SYSTEM_PROMPT,
5405
5789
  tools=tools,
5406
- plugins=plugins,{{#if hasConfigBundle}}
5790
+ plugins=plugins{{#if hasSkillsFetcher}} + _skill_plugins{{/if}},{{#if hasConfigBundle}}
5407
5791
  hooks=[ConfigBundleHook()],{{/if}}
5408
5792
  )
5409
5793
  {{else}}
5410
5794
  {{#if hasConfigBundle}}
5411
- agent = create_agent()
5795
+ agent = create_agent({{#if hasSkillsFetcher}}_skill_plugins{{/if}})
5412
5796
  {{else}}
5413
- agent = get_or_create_agent()
5797
+ agent = get_or_create_agent({{#if hasSkillsFetcher}}_skill_plugins{{/if}})
5414
5798
  {{/if}}
5415
5799
  {{/if}}
5416
5800
  {{/if}}
5417
5801
 
5418
- # Execute and format response
5419
- stream = agent.stream_async(payload.get("prompt"))
5802
+ prompt = _extract_prompt(payload)
5803
+
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}}
5420
5813
 
5421
- async for event in stream:
5422
- # Handle Text parts of the response
5423
- if "data" in event and isinstance(event["data"], str):
5424
- yield event["data"]
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}}
5425
5884
 
5426
5885
 
5427
5886
  if __name__ == "__main__":
@@ -5466,18 +5925,22 @@ def _get_bearer_token_{{snakeCase name}}(*, access_token: str):
5466
5925
  {{#each gatewayProviders}}
5467
5926
  def get_{{snakeCase name}}_mcp_client() -> MCPClient | None:
5468
5927
  """Returns an MCP Client connected to the {{name}} gateway."""
5928
+ {{#if hardcodedUrl}}
5929
+ url = {{safeJson hardcodedUrl}}
5930
+ {{else}}
5469
5931
  url = os.environ.get("{{envVarName}}")
5470
5932
  if not url:
5471
5933
  logger.warning("{{envVarName}} not set — {{name}} gateway tools unavailable")
5472
5934
  return None
5935
+ {{/if}}
5473
5936
  {{#if (eq authType "AWS_IAM")}}
5474
- 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}}")
5475
5938
  {{else if (eq authType "CUSTOM_JWT")}}
5476
5939
  token = _get_bearer_token_{{snakeCase name}}()
5477
5940
  headers = {"Authorization": f"Bearer {token}"} if token else {}
5478
- return MCPClient(lambda: streamablehttp_client(url, headers=headers))
5941
+ return MCPClient(lambda: streamablehttp_client(url, headers=headers), prefix="{{snakeCase name}}")
5479
5942
  {{else}}
5480
- return MCPClient(lambda: streamablehttp_client(url))
5943
+ return MCPClient(lambda: streamablehttp_client(url), prefix="{{snakeCase name}}")
5481
5944
  {{/if}}
5482
5945
 
5483
5946
  {{/each}}
@@ -5490,7 +5953,41 @@ def get_all_gateway_mcp_clients() -> list[MCPClient]:
5490
5953
  clients.append(client)
5491
5954
  {{/each}}
5492
5955
  return clients
5493
- {{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)}}
5494
5991
  {{#if isVpc}}
5495
5992
  # VPC mode: external MCP endpoints are not reachable without a NAT gateway.
5496
5993
  # Add an AgentCore Gateway with \`agentcore add gateway\`, or configure your own endpoint below.
@@ -5499,6 +5996,7 @@ def get_streamable_http_mcp_client() -> MCPClient | None:
5499
5996
  """No MCP server configured. Add a gateway with \`agentcore add gateway\`."""
5500
5997
  return None
5501
5998
  {{else}}
5999
+ {{#unless isExportHarness}}
5502
6000
  # ExaAI provides information about code through web searches, crawling and code context searches through their platform. Requires no authentication
5503
6001
  EXAMPLE_MCP_ENDPOINT = "https://mcp.exa.ai/mcp"
5504
6002
 
@@ -5506,8 +6004,9 @@ def get_streamable_http_mcp_client() -> MCPClient:
5506
6004
  """Returns an MCP Client compatible with Strands"""
5507
6005
  # to use an MCP server that supports bearer authentication, add headers={"Authorization": f"Bearer {access_token}"}
5508
6006
  return MCPClient(lambda: streamablehttp_client(EXAMPLE_MCP_ENDPOINT))
6007
+ {{/unless}}
5509
6008
  {{/if}}
5510
- {{/if}}
6009
+ {{/unless}}
5511
6010
  "
5512
6011
  `;
5513
6012
 
@@ -5523,7 +6022,7 @@ from strands.models.bedrock import BedrockModel
5523
6022
 
5524
6023
  def load_model() -> BedrockModel:
5525
6024
  """Get Bedrock model client using IAM credentials."""
5526
- 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}}")
5527
6026
  {{/if}}
5528
6027
  {{#if (eq modelProvider "Anthropic")}}
5529
6028
  import os
@@ -5599,7 +6098,7 @@ def load_model() -> OpenAIModel:
5599
6098
  """Get authenticated OpenAI model client."""
5600
6099
  return OpenAIModel(
5601
6100
  client_args={"api_key": _get_api_key()},
5602
- model_id="gpt-4.1",
6101
+ model_id="{{#if modelId}}{{modelId}}{{else}}gpt-4.1{{/if}}",
5603
6102
  )
5604
6103
  {{/if}}
5605
6104
  {{#if (eq modelProvider "Gemini")}}
@@ -5637,7 +6136,7 @@ def load_model() -> GeminiModel:
5637
6136
  """Get authenticated Gemini model client."""
5638
6137
  return GeminiModel(
5639
6138
  client_args={"api_key": _get_api_key()},
5640
- model_id="gemini-2.5-flash",
6139
+ model_id="{{#if modelId}}{{modelId}}{{else}}gemini-2.5-flash{{/if}}",
5641
6140
  )
5642
6141
  {{/if}}
5643
6142
  "
@@ -5662,8 +6161,11 @@ dependencies = [
5662
6161
  {{#if (eq modelProvider "Gemini")}}"google-genai >= 1.0.0",
5663
6162
  {{/if}}"mcp >= 1.19.0",
5664
6163
  {{#if (eq modelProvider "OpenAI")}}"openai >= 1.0.0",
5665
- {{/if}}"strands-agents >= 1.13.0",
5666
- {{#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",
5667
6169
  {{/if}}{{/if}}
5668
6170
  ]
5669
6171
 
@@ -5672,6 +6174,347 @@ packages = ["."]
5672
6174
  "
5673
6175
  `;
5674
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
+
5675
6518
  exports[`Assets Directory Snapshots > Python framework assets > python/python/http/strands/capabilities/memory/__init__.py should match snapshot 1`] = `
5676
6519
  "# Package marker
5677
6520
  "
@@ -5704,8 +6547,11 @@ def get_memory_session_manager(session_id: Optional[str], actor_id: str) -> Opti
5704
6547
  {{#if (includes memoryProviders.[0].strategies "USER_PREFERENCE")}}
5705
6548
  f"/users/{actor_id}/preferences": RetrievalConfig(top_k=3, relevance_score=0.5),
5706
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}}
5707
6553
  {{#if (includes memoryProviders.[0].strategies "SUMMARIZATION")}}
5708
- 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),
5709
6555
  {{/if}}
5710
6556
  }
5711
6557
  {{/if}}
@@ -6368,6 +7214,19 @@ When modifying JSON config files:
6368
7214
  4. Use CloudFormation-safe names (alphanumeric, start with letter)
6369
7215
  5. Run \`agentcore validate\` to verify changes
6370
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
+
6371
7230
  ## CLI Commands
6372
7231
 
6373
7232
  | Command | Description |
@@ -6375,6 +7234,7 @@ When modifying JSON config files:
6375
7234
  | \`agentcore create\` | Create a new project |
6376
7235
  | \`agentcore add <resource>\` | Add agent, memory, credential, gateway, evaluator, policy |
6377
7236
  | \`agentcore remove <resource>\` | Remove a resource |
7237
+ | \`agentcore export harness\` | Export a harness to a Strands runtime agent |
6378
7238
  | \`agentcore dev\` | Run agent locally with hot-reload |
6379
7239
  | \`agentcore deploy\` | Deploy to AWS |
6380
7240
  | \`agentcore status\` | Show deployment status |