@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.
- package/README.md +31 -17
- package/dist/agent-inspector/index.css +1 -1
- package/dist/agent-inspector/index.js +90 -85
- package/dist/assets/__tests__/__snapshots__/assets.snapshot.test.ts.snap +966 -106
- package/dist/assets/agents/AGENTS.md +14 -0
- package/dist/assets/cdk/bin/cdk.ts +42 -20
- package/dist/assets/cdk/lib/cdk-stack.ts +19 -14
- package/dist/assets/cdk/test/cdk.test.ts +2 -1
- package/dist/assets/container/python/Dockerfile +4 -0
- package/dist/assets/python/a2a/strands/capabilities/memory/session.py +4 -1
- package/dist/assets/python/agui/strands/capabilities/memory/session.py +4 -1
- package/dist/assets/python/http/googleadk/base/mcp_client/client.py +7 -7
- package/dist/assets/python/http/googleadk/base/pyproject.toml +1 -1
- package/dist/assets/python/http/langchain_langgraph/base/mcp_client/client.py +10 -8
- package/dist/assets/python/http/langchain_langgraph/base/pyproject.toml +1 -1
- package/dist/assets/python/http/openaiagents/base/main.py +19 -9
- package/dist/assets/python/http/openaiagents/base/mcp_client/client.py +4 -4
- package/dist/assets/python/http/openaiagents/base/pyproject.toml +1 -1
- package/dist/assets/python/http/strands/base/main.py +438 -27
- package/dist/assets/python/http/strands/base/mcp_client/client.py +45 -5
- package/dist/assets/python/http/strands/base/model/load.py +3 -3
- package/dist/assets/python/http/strands/base/pyproject.toml +5 -2
- package/dist/assets/python/http/strands/base/skills/fetcher.py +279 -0
- package/dist/assets/python/http/strands/capabilities/execution-limits/hooks/execution_limits.py +54 -0
- package/dist/assets/python/http/strands/capabilities/memory/session.py +4 -1
- package/dist/cli/index.mjs +1274 -1088
- package/dist/lib/errors/types.d.ts +18 -0
- package/dist/lib/errors/types.d.ts.map +1 -1
- package/dist/lib/errors/types.js +28 -1
- package/dist/lib/errors/types.js.map +1 -1
- package/dist/lib/schemas/io/config-io.d.ts.map +1 -1
- package/dist/lib/schemas/io/config-io.js +0 -2
- package/dist/lib/schemas/io/config-io.js.map +1 -1
- package/dist/schema/constants.d.ts +20 -0
- package/dist/schema/constants.d.ts.map +1 -1
- package/dist/schema/constants.js +25 -1
- package/dist/schema/constants.js.map +1 -1
- package/dist/schema/schemas/agent-env.d.ts +35 -0
- package/dist/schema/schemas/agent-env.d.ts.map +1 -1
- package/dist/schema/schemas/agentcore-project.d.ts +184 -25
- package/dist/schema/schemas/agentcore-project.d.ts.map +1 -1
- package/dist/schema/schemas/agentcore-project.js +81 -31
- package/dist/schema/schemas/agentcore-project.js.map +1 -1
- package/dist/schema/schemas/auth.d.ts +175 -0
- package/dist/schema/schemas/auth.d.ts.map +1 -1
- package/dist/schema/schemas/auth.js +126 -3
- package/dist/schema/schemas/auth.js.map +1 -1
- package/dist/schema/schemas/deployed-state.d.ts +140 -46
- package/dist/schema/schemas/deployed-state.d.ts.map +1 -1
- package/dist/schema/schemas/deployed-state.js +39 -14
- package/dist/schema/schemas/deployed-state.js.map +1 -1
- package/dist/schema/schemas/mcp-defs.d.ts +2 -1
- package/dist/schema/schemas/mcp-defs.d.ts.map +1 -1
- package/dist/schema/schemas/mcp-defs.js +3 -2
- package/dist/schema/schemas/mcp-defs.js.map +1 -1
- package/dist/schema/schemas/mcp.d.ts +186 -0
- package/dist/schema/schemas/mcp.d.ts.map +1 -1
- package/dist/schema/schemas/mcp.js +479 -4
- package/dist/schema/schemas/mcp.js.map +1 -1
- package/dist/schema/schemas/primitives/ab-test.d.ts +0 -6
- package/dist/schema/schemas/primitives/ab-test.d.ts.map +1 -1
- package/dist/schema/schemas/primitives/ab-test.js +0 -2
- package/dist/schema/schemas/primitives/ab-test.js.map +1 -1
- package/dist/schema/schemas/primitives/harness.d.ts +209 -11
- package/dist/schema/schemas/primitives/harness.d.ts.map +1 -1
- package/dist/schema/schemas/primitives/harness.js +268 -21
- package/dist/schema/schemas/primitives/harness.js.map +1 -1
- package/dist/schema/schemas/primitives/index.d.ts +6 -8
- package/dist/schema/schemas/primitives/index.d.ts.map +1 -1
- package/dist/schema/schemas/primitives/index.js +6 -5
- package/dist/schema/schemas/primitives/index.js.map +1 -1
- package/dist/schema/schemas/primitives/knowledge-base.d.ts +104 -0
- package/dist/schema/schemas/primitives/knowledge-base.d.ts.map +1 -0
- package/dist/schema/schemas/primitives/knowledge-base.js +99 -0
- package/dist/schema/schemas/primitives/knowledge-base.js.map +1 -0
- package/dist/schema/schemas/primitives/online-eval-config.d.ts +20 -2
- package/dist/schema/schemas/primitives/online-eval-config.d.ts.map +1 -1
- package/dist/schema/schemas/primitives/online-eval-config.js +43 -5
- package/dist/schema/schemas/primitives/online-eval-config.js.map +1 -1
- package/dist/schema/schemas/primitives/policy.d.ts +26 -0
- package/dist/schema/schemas/primitives/policy.d.ts.map +1 -1
- package/dist/schema/schemas/primitives/policy.js +5 -1
- package/dist/schema/schemas/primitives/policy.js.map +1 -1
- package/npm-shrinkwrap.json +195 -330
- package/package.json +10 -8
- package/scripts/bundle.mjs +51 -0
- package/scripts/run-e2e-local.sh +40 -8
- package/dist/schema/schemas/primitives/http-gateway.d.ts +0 -21
- package/dist/schema/schemas/primitives/http-gateway.d.ts.map +0 -1
- package/dist/schema/schemas/primitives/http-gateway.js +0 -34
- 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
|
|
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
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
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
|
-
|
|
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
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
4015
|
-
toolsets.append(MCPToolset(connection_params=StreamableHTTPConnectionParams(url=url)))
|
|
4016
|
-
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
4447
|
-
servers["{{name}}"] = {"transport": "streamable_http", "url": url}
|
|
4448
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
4861
|
-
|
|
4862
|
-
|
|
4863
|
-
|
|
4864
|
-
mcp_servers
|
|
4865
|
-
|
|
4866
|
-
|
|
4867
|
-
|
|
4868
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
5027
|
+
{{else}}
|
|
4980
5028
|
servers.append(MCPServerStreamableHttp(name="{{name}}", params={"url": url}))
|
|
4981
|
-
|
|
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.
|
|
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
|
-
{{#
|
|
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
|
-
#
|
|
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
|
|
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
|
-
{{
|
|
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
|
-
|
|
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,
|
|
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
|
|
5338
|
-
|
|
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
|
-
|
|
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
|
-
|
|
5419
|
-
|
|
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
|
-
|
|
5422
|
-
|
|
5423
|
-
|
|
5424
|
-
|
|
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
|
-
{{
|
|
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
|
-
{{/
|
|
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.
|
|
5666
|
-
{{#if
|
|
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 |
|