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