@aws/agentcore 1.0.0-preview.3 → 1.0.0-preview.30
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/README.md +6 -2
- package/dist/assets/__tests__/__snapshots__/assets.snapshot.test.ts.snap +3344 -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-observability.test.ts +23 -0
- package/dist/assets/__tests__/googleadk-session-eviction.test.ts +87 -0
- package/dist/assets/__tests__/input-validation.test.ts +44 -0
- package/dist/assets/__tests__/summarization-namespace.test.ts +43 -0
- package/dist/assets/agents/AGENTS.md +91 -14
- package/dist/assets/cdk/bin/cdk.ts +109 -24
- package/dist/assets/cdk/lib/cdk-stack.ts +227 -19
- package/dist/assets/cdk/package.json +9 -9
- 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/evaluators/autoevals-lambda/execution-role-policy.json +15 -0
- package/dist/assets/evaluators/autoevals-lambda/lambda_function.py +37 -0
- package/dist/assets/evaluators/autoevals-lambda/pyproject.toml +22 -0
- package/dist/assets/evaluators/deepeval-lambda/execution-role-policy.json +15 -0
- package/dist/assets/evaluators/deepeval-lambda/lambda_function.py +29 -0
- package/dist/assets/evaluators/deepeval-lambda/pyproject.toml +19 -0
- package/dist/assets/mcp/python/pyproject.toml +1 -1
- package/dist/assets/python/a2a/googleadk/base/main.py +32 -18
- package/dist/assets/python/a2a/googleadk/base/pyproject.toml +6 -4
- package/dist/assets/python/a2a/langchain_langgraph/base/main.py +35 -21
- package/dist/assets/python/a2a/langchain_langgraph/base/pyproject.toml +4 -3
- package/dist/assets/python/a2a/strands/base/main.py +23 -17
- package/dist/assets/python/a2a/strands/base/pyproject.toml +3 -3
- package/dist/assets/python/a2a/strands/capabilities/memory/session.py +4 -1
- package/dist/assets/python/agui/googleadk/base/main.py +62 -2
- package/dist/assets/python/agui/googleadk/base/pyproject.toml +6 -5
- package/dist/assets/python/agui/langchain_langgraph/base/main.py +56 -0
- package/dist/assets/python/agui/langchain_langgraph/base/pyproject.toml +3 -2
- package/dist/assets/python/agui/strands/base/main.py +63 -1
- package/dist/assets/python/agui/strands/base/pyproject.toml +2 -2
- package/dist/assets/python/agui/strands/capabilities/memory/session.py +4 -1
- package/dist/assets/python/http/autogen/base/README.md +7 -1
- package/dist/assets/python/http/autogen/base/main.py +62 -29
- package/dist/assets/python/http/autogen/base/pyproject.toml +1 -1
- package/dist/assets/python/http/googleadk/base/README.md +7 -1
- package/dist/assets/python/http/googleadk/base/main.py +71 -24
- package/dist/assets/python/http/googleadk/base/mcp_client/client.py +7 -7
- package/dist/assets/python/http/googleadk/base/pyproject.toml +5 -3
- package/dist/assets/python/http/langchain_langgraph/base/README.md +7 -1
- package/dist/assets/python/http/langchain_langgraph/base/main.py +119 -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 +5 -4
- package/dist/assets/python/http/openaiagents/base/README.md +7 -1
- package/dist/assets/python/http/openaiagents/base/main.py +63 -32
- package/dist/assets/python/http/openaiagents/base/mcp_client/client.py +4 -4
- package/dist/assets/python/http/openaiagents/base/pyproject.toml +3 -3
- package/dist/assets/python/http/strands/base/README.md +8 -1
- package/dist/assets/python/http/strands/base/main.py +607 -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 +11 -5
- package/dist/assets/python/http/strands/base/skills/fetcher.py +279 -0
- package/dist/assets/python/http/strands/capabilities/execution-limits/hooks/execution_limits.py +54 -0
- package/dist/assets/python/http/strands/capabilities/memory/session.py +4 -1
- package/dist/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/python/mcp/standalone/base/pyproject.toml +1 -1
- package/dist/assets/typescript/http/strands/base/README.md +42 -0
- package/dist/assets/typescript/http/strands/base/gitignore.template +22 -0
- package/dist/assets/typescript/http/strands/base/main.ts +149 -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 +42 -0
- package/dist/assets/typescript/http/vercelai/base/gitignore.template +22 -0
- package/dist/assets/typescript/http/vercelai/base/main.ts +70 -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 +1743 -627
- 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/dependency-management/index.d.ts +5 -0
- package/dist/lib/dependency-management/index.d.ts.map +1 -0
- package/dist/lib/dependency-management/index.js +8 -0
- package/dist/lib/dependency-management/index.js.map +1 -0
- package/dist/lib/dependency-management/messages.d.ts +30 -0
- package/dist/lib/dependency-management/messages.d.ts.map +1 -0
- package/dist/lib/dependency-management/messages.js +86 -0
- package/dist/lib/dependency-management/messages.js.map +1 -0
- package/dist/lib/dependency-management/plan.d.ts +25 -0
- package/dist/lib/dependency-management/plan.d.ts.map +1 -0
- package/dist/lib/dependency-management/plan.js +67 -0
- package/dist/lib/dependency-management/plan.js.map +1 -0
- package/dist/lib/dependency-management/semver.d.ts +21 -0
- package/dist/lib/dependency-management/semver.d.ts.map +1 -0
- package/dist/lib/dependency-management/semver.js +51 -0
- package/dist/lib/dependency-management/semver.js.map +1 -0
- package/dist/lib/dependency-management/sync.d.ts +26 -0
- package/dist/lib/dependency-management/sync.d.ts.map +1 -0
- package/dist/lib/dependency-management/sync.js +141 -0
- package/dist/lib/dependency-management/sync.js.map +1 -0
- package/dist/lib/dependency-management/types.d.ts +85 -0
- package/dist/lib/dependency-management/types.d.ts.map +1 -0
- package/dist/lib/dependency-management/types.js +3 -0
- package/dist/lib/dependency-management/types.js.map +1 -0
- 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 +274 -0
- package/dist/lib/errors/types.d.ts.map +1 -0
- package/dist/lib/errors/types.js +423 -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 +57 -0
- package/dist/lib/result.d.ts.map +1 -0
- package/dist/lib/result.js +62 -0
- package/dist/lib/result.js.map +1 -0
- package/dist/lib/schemas/io/config-io.d.ts +14 -0
- package/dist/lib/schemas/io/config-io.d.ts.map +1 -1
- package/dist/lib/schemas/io/config-io.js +89 -5
- package/dist/lib/schemas/io/config-io.js.map +1 -1
- package/dist/lib/schemas/io/global-config.d.ts +41 -0
- package/dist/lib/schemas/io/global-config.d.ts.map +1 -0
- package/dist/lib/schemas/io/global-config.js +119 -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/subprocess.d.ts +4 -1
- package/dist/lib/utils/subprocess.d.ts.map +1 -1
- package/dist/lib/utils/subprocess.js +11 -57
- package/dist/lib/utils/subprocess.js.map +1 -1
- 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 +187 -7
- package/dist/schema/schemas/agent-env.d.ts.map +1 -1
- package/dist/schema/schemas/agent-env.js +323 -27
- package/dist/schema/schemas/agent-env.js.map +1 -1
- package/dist/schema/schemas/agentcore-project.d.ts +519 -12
- package/dist/schema/schemas/agentcore-project.d.ts.map +1 -1
- package/dist/schema/schemas/agentcore-project.js +342 -13
- 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 +18 -0
- package/dist/schema/schemas/aws-targets.d.ts.map +1 -1
- package/dist/schema/schemas/aws-targets.js +6 -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 +392 -5
- package/dist/schema/schemas/deployed-state.d.ts.map +1 -1
- package/dist/schema/schemas/deployed-state.js +112 -2
- package/dist/schema/schemas/deployed-state.js.map +1 -1
- package/dist/schema/schemas/index.d.ts +3 -0
- package/dist/schema/schemas/index.d.ts.map +1 -1
- package/dist/schema/schemas/index.js +5 -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 +197 -0
- package/dist/schema/schemas/mcp.d.ts.map +1 -1
- package/dist/schema/schemas/mcp.js +334 -5
- 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/capacity-provider.d.ts +237 -0
- package/dist/schema/schemas/primitives/capacity-provider.d.ts.map +1 -0
- package/dist/schema/schemas/primitives/capacity-provider.js +220 -0
- package/dist/schema/schemas/primitives/capacity-provider.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 +22 -0
- package/dist/schema/schemas/primitives/evaluator.d.ts.map +1 -1
- package/dist/schema/schemas/primitives/evaluator.js +28 -3
- 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 +17 -9
- package/dist/schema/schemas/primitives/index.d.ts.map +1 -1
- package/dist/schema/schemas/primitives/index.js +62 -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 +86 -0
- package/dist/schema/schemas/primitives/payment.d.ts.map +1 -0
- package/dist/schema/schemas/primitives/payment.js +88 -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 +16874 -0
- package/package.json +49 -21
- package/scripts/bump-version.ts +4 -4
- package/scripts/bundle.mjs +70 -41
- package/scripts/extract-cli-model.mjs +269 -0
- package/scripts/extract-cli-model.test.mjs +43 -0
- package/scripts/render_adoc.py +350 -0
- package/scripts/run-e2e-dev.sh +56 -0
- package/scripts/run-e2e-local.sh +145 -0
- package/scripts/safe-junit-reporter.ts +7 -0
- package/scripts/sanitize-test-artifacts.ts +65 -0
- package/scripts/sync-vended-cdk.ts +49 -0
- package/scripts/test_render_adoc.py +38 -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
142
|
dockerfile: harnessSpec.dockerfile,
|
|
130
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,76 @@ 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: {
|
|
190
|
+
name: string;
|
|
191
|
+
provider?: string;
|
|
192
|
+
provisionMode?: 'MANUAL' | 'QUICK_CREATE';
|
|
193
|
+
credentialName?: string;
|
|
194
|
+
}[];
|
|
195
|
+
}) => ({
|
|
196
|
+
name: p.name,
|
|
197
|
+
description: p.description,
|
|
198
|
+
authorizerType: p.authorizerType,
|
|
199
|
+
authorizerConfiguration: p.authorizerConfiguration,
|
|
200
|
+
autoPayment: p.autoPayment,
|
|
201
|
+
paymentToolAllowlist: p.paymentToolAllowlist,
|
|
202
|
+
networkPreferences: p.networkPreferences,
|
|
203
|
+
connectors: p.connectors.map(c => {
|
|
204
|
+
if (c.provisionMode === 'QUICK_CREATE') {
|
|
205
|
+
return {
|
|
206
|
+
name: c.name,
|
|
207
|
+
provider: 'CoinbaseCDP' as const,
|
|
208
|
+
provisionMode: 'QUICK_CREATE' as const,
|
|
209
|
+
};
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
if (!c.credentialName) {
|
|
213
|
+
throw new Error(
|
|
214
|
+
\`Manual payment connector "\${c.name}" on manager "\${p.name}" is missing its credential name.\`
|
|
215
|
+
);
|
|
216
|
+
}
|
|
217
|
+
const credentialProviderArn = paymentCredentials?.[c.credentialName]?.credentialProviderArn;
|
|
218
|
+
if (!credentialProviderArn) {
|
|
219
|
+
// Fail fast with an actionable message rather than passing an empty
|
|
220
|
+
// ARN that fails opaquely server-side at CreatePaymentConnector.
|
|
221
|
+
throw new Error(
|
|
222
|
+
\`Payment connector "\${c.name}" on manager "\${p.name}" references credential \` +
|
|
223
|
+
\`"\${c.credentialName}", but no deployed credential provider was found for it. \` +
|
|
224
|
+
\`Run \\\`agentcore deploy\\\` so the credential provider is created first.\`
|
|
225
|
+
);
|
|
226
|
+
}
|
|
227
|
+
return {
|
|
228
|
+
name: c.name,
|
|
229
|
+
provider: c.provider,
|
|
230
|
+
...(c.provisionMode && { provisionMode: c.provisionMode }),
|
|
231
|
+
credentialName: c.credentialName,
|
|
232
|
+
credentialProviderArn,
|
|
233
|
+
};
|
|
234
|
+
}),
|
|
235
|
+
})
|
|
236
|
+
)
|
|
237
|
+
: undefined;
|
|
238
|
+
|
|
156
239
|
new AgentCoreStack(app, stackName, {
|
|
157
240
|
spec,
|
|
158
241
|
mcpSpec,
|
|
159
242
|
credentials,
|
|
243
|
+
connectorParametersByFile,
|
|
160
244
|
harnesses: harnessConfigs.length > 0 ? harnessConfigs : undefined,
|
|
245
|
+
paymentSpec,
|
|
161
246
|
env,
|
|
162
247
|
description: \`AgentCore stack for \${spec.name} deployed to \${target.name} (\${target.region})\`,
|
|
163
248
|
tags: {
|
|
@@ -172,7 +257,7 @@ async function main() {
|
|
|
172
257
|
|
|
173
258
|
main().catch((error: unknown) => {
|
|
174
259
|
console.error('AgentCore CDK synthesis failed:', error instanceof Error ? error.message : error);
|
|
175
|
-
process.
|
|
260
|
+
process.exit(1);
|
|
176
261
|
});
|
|
177
262
|
"
|
|
178
263
|
`;
|
|
@@ -299,22 +384,51 @@ exports[`Assets Directory Snapshots > CDK assets > cdk/cdk/lib/cdk-stack.ts shou
|
|
|
299
384
|
"import {
|
|
300
385
|
AgentCoreApplication,
|
|
301
386
|
AgentCoreMcp,
|
|
387
|
+
AgentCorePaymentManager,
|
|
388
|
+
AgentCorePaymentConnector,
|
|
302
389
|
type AgentCoreProjectSpec,
|
|
303
390
|
type AgentCoreMcpSpec,
|
|
391
|
+
type CustomJWTAuthorizerConfig,
|
|
392
|
+
type HarnessDeploymentConfig,
|
|
304
393
|
} from '@aws/agentcore-cdk';
|
|
305
394
|
import { CfnOutput, Stack, type StackProps } from 'aws-cdk-lib';
|
|
395
|
+
import * as iam from 'aws-cdk-lib/aws-iam';
|
|
306
396
|
import { Construct } from 'constructs';
|
|
307
397
|
|
|
308
|
-
|
|
398
|
+
/**
|
|
399
|
+
* Harness deployment config: role-scoped fields (for IAM role + container build)
|
|
400
|
+
* plus the full validated spec + its config directory so the L3 construct can
|
|
401
|
+
* synthesize the AWS::BedrockAgentCore::Harness resource.
|
|
402
|
+
*/
|
|
403
|
+
export type HarnessConfig = HarnessDeploymentConfig;
|
|
404
|
+
|
|
405
|
+
export interface ManualPaymentConnectorSpec {
|
|
406
|
+
name: string;
|
|
407
|
+
provider: 'CoinbaseCDP' | 'StripePrivy';
|
|
408
|
+
provisionMode?: 'MANUAL';
|
|
409
|
+
credentialName: string;
|
|
410
|
+
credentialProviderArn: string;
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
export interface QuickCreatePaymentConnectorSpec {
|
|
414
|
+
name: string;
|
|
415
|
+
provider: 'CoinbaseCDP';
|
|
416
|
+
provisionMode: 'QUICK_CREATE';
|
|
417
|
+
credentialName?: never;
|
|
418
|
+
credentialProviderArn?: never;
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
export type PaymentConnectorSpec = ManualPaymentConnectorSpec | QuickCreatePaymentConnectorSpec;
|
|
422
|
+
|
|
423
|
+
export interface PaymentSpec {
|
|
309
424
|
name: string;
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
apiKeyArn?: string;
|
|
425
|
+
description?: string;
|
|
426
|
+
authorizerType: 'AWS_IAM' | 'CUSTOM_JWT';
|
|
427
|
+
authorizerConfiguration?: { customJWTAuthorizer: CustomJWTAuthorizerConfig };
|
|
428
|
+
autoPayment?: boolean;
|
|
429
|
+
paymentToolAllowlist?: string[];
|
|
430
|
+
networkPreferences?: string[];
|
|
431
|
+
connectors: PaymentConnectorSpec[];
|
|
318
432
|
}
|
|
319
433
|
|
|
320
434
|
export interface AgentCoreStackProps extends StackProps {
|
|
@@ -331,13 +445,38 @@ export interface AgentCoreStackProps extends StackProps {
|
|
|
331
445
|
*/
|
|
332
446
|
credentials?: Record<string, { credentialProviderArn: string; clientSecretArn?: string }>;
|
|
333
447
|
/**
|
|
334
|
-
* Harness role configurations.
|
|
335
|
-
*
|
|
336
|
-
* When \`hasDockerfile\` is true and \`codeLocation\` is provided (without an explicit
|
|
337
|
-
* \`containerUri\`), the L3 construct builds and pushes a container image via CodeBuild
|
|
338
|
-
* and emits its URI as a stack output for the post-CDK harness deployer.
|
|
448
|
+
* Harness role configurations.
|
|
339
449
|
*/
|
|
340
450
|
harnesses?: HarnessConfig[];
|
|
451
|
+
/**
|
|
452
|
+
* Parsed connectorParameters for non-S3 KB data sources, keyed by
|
|
453
|
+
* connectorConfigFile path. Forwarded to AgentCoreApplication.
|
|
454
|
+
*/
|
|
455
|
+
connectorParametersByFile?: Record<string, Record<string, unknown>>;
|
|
456
|
+
/**
|
|
457
|
+
* Payment specifications with resolved credential provider ARNs.
|
|
458
|
+
*/
|
|
459
|
+
paymentSpec?: PaymentSpec[];
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
function toCdkId(name: string): string {
|
|
463
|
+
return name.replace(/_/g, '');
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
/**
|
|
467
|
+
* Decide whether a deployed runtime should receive payment env vars + IAM grants.
|
|
468
|
+
* Payments today only ships a runtime shim for Python HTTP runtimes; injecting
|
|
469
|
+
* AGENTCORE_PAYMENT_* env vars into TypeScript / MCP / A2A / AGUI runtimes
|
|
470
|
+
* would surface env vars they cannot consume and would dilute least-privilege
|
|
471
|
+
* IAM grants for runtimes that never call ProcessPayment.
|
|
472
|
+
*/
|
|
473
|
+
function isPaymentEligibleAgent(agent: { entrypoint?: string; protocol?: string }): boolean {
|
|
474
|
+
if (agent.protocol && agent.protocol !== 'HTTP') {
|
|
475
|
+
return false;
|
|
476
|
+
}
|
|
477
|
+
const entrypoint = typeof agent.entrypoint === 'string' ? agent.entrypoint : '';
|
|
478
|
+
const entrypointFile = entrypoint.split(':')[0] ?? '';
|
|
479
|
+
return entrypointFile.endsWith('.py');
|
|
341
480
|
}
|
|
342
481
|
|
|
343
482
|
/**
|
|
@@ -353,13 +492,21 @@ export class AgentCoreStack extends Stack {
|
|
|
353
492
|
constructor(scope: Construct, id: string, props: AgentCoreStackProps) {
|
|
354
493
|
super(scope, id, props);
|
|
355
494
|
|
|
356
|
-
const { spec, mcpSpec, credentials, harnesses } = props;
|
|
495
|
+
const { spec, mcpSpec, credentials, harnesses, connectorParametersByFile, paymentSpec } = props;
|
|
357
496
|
|
|
358
497
|
// Create AgentCoreApplication with all agents and harness roles
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
498
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
499
|
+
const appProps: Record<string, unknown> = { spec };
|
|
500
|
+
if (harnesses?.length) {
|
|
501
|
+
appProps.harnesses = harnesses;
|
|
502
|
+
}
|
|
503
|
+
if (connectorParametersByFile && Object.keys(connectorParametersByFile).length > 0) {
|
|
504
|
+
appProps.connectorParametersByFile = connectorParametersByFile;
|
|
505
|
+
}
|
|
506
|
+
if (credentials) {
|
|
507
|
+
appProps.credentials = credentials;
|
|
508
|
+
}
|
|
509
|
+
this.application = new AgentCoreApplication(this, 'Application', appProps as any);
|
|
363
510
|
|
|
364
511
|
// Create AgentCoreMcp if there are gateways configured
|
|
365
512
|
if (mcpSpec?.agentCoreGateways && mcpSpec.agentCoreGateways.length > 0) {
|
|
@@ -372,6 +519,152 @@ export class AgentCoreStack extends Stack {
|
|
|
372
519
|
});
|
|
373
520
|
}
|
|
374
521
|
|
|
522
|
+
// Create payment infrastructure via CFN constructs
|
|
523
|
+
if (paymentSpec && paymentSpec.length > 0) {
|
|
524
|
+
for (const payment of paymentSpec) {
|
|
525
|
+
const mgrId = toCdkId(payment.name);
|
|
526
|
+
const manager = new AgentCorePaymentManager(this, \`Payment\${mgrId}\`, {
|
|
527
|
+
projectName: spec.name,
|
|
528
|
+
name: payment.name,
|
|
529
|
+
authorizerType: payment.authorizerType,
|
|
530
|
+
description: payment.description,
|
|
531
|
+
authorizerConfiguration: payment.authorizerConfiguration,
|
|
532
|
+
tags: spec.tags,
|
|
533
|
+
});
|
|
534
|
+
|
|
535
|
+
const prefix = \`AGENTCORE_PAYMENT_\${payment.name.toUpperCase().replace(/-/g, '_')}\`;
|
|
536
|
+
|
|
537
|
+
// Wire env vars from construct output tokens into eligible agent environments only.
|
|
538
|
+
// See isPaymentEligibleAgent — non-Python or non-HTTP runtimes have no shim that
|
|
539
|
+
// can consume these env vars, and giving them sts:AssumeRole on the
|
|
540
|
+
// ProcessPaymentRole would broaden the privilege surface unnecessarily.
|
|
541
|
+
for (const env of this.application.environments.values()) {
|
|
542
|
+
if (!isPaymentEligibleAgent(env.agent)) {
|
|
543
|
+
continue;
|
|
544
|
+
}
|
|
545
|
+
env.runtime.addEnvironmentVariable(\`\${prefix}_MANAGER_ARN\`, manager.paymentManagerArn);
|
|
546
|
+
env.runtime.addEnvironmentVariable(\`\${prefix}_PROCESS_PAYMENT_ROLE_ARN\`, manager.processPaymentRoleArn);
|
|
547
|
+
|
|
548
|
+
// Grant runtime execution role permission to assume the ProcessPaymentRole.
|
|
549
|
+
// The ProcessPaymentRole's trust policy allows AccountRootPrincipal, but the
|
|
550
|
+
// caller still needs sts:AssumeRole on its own role to perform the assumption.
|
|
551
|
+
env.runtime.role.addToPrincipalPolicy(
|
|
552
|
+
new iam.PolicyStatement({
|
|
553
|
+
actions: ['sts:AssumeRole'],
|
|
554
|
+
resources: [manager.processPaymentRoleArn],
|
|
555
|
+
})
|
|
556
|
+
);
|
|
557
|
+
|
|
558
|
+
// Grant payment data-plane actions directly to the runtime role.
|
|
559
|
+
//
|
|
560
|
+
// NOTE: This deviates from the canonical role model in the AgentCore Payments
|
|
561
|
+
// beta guide, which assigns Get/List/Create instrument+session actions to a
|
|
562
|
+
// separate ManagementRole and limits the agent's role to ProcessPayment only.
|
|
563
|
+
// The current SDK plugin (AgentCorePaymentsPlugin.generate_payment_header)
|
|
564
|
+
// calls GetPaymentInstrument internally during the 402 auto-pay path, so the
|
|
565
|
+
// runtime role needs read access. CreatePaymentSession is included so
|
|
566
|
+
// \`agentcore invoke --auto-session\` works without a separate ManagementRole
|
|
567
|
+
// call. Tighten this if the SDK is updated to accept pre-fetched instrument
|
|
568
|
+
// details and split create-session into a backend-only flow.
|
|
569
|
+
env.runtime.role.addToPrincipalPolicy(
|
|
570
|
+
new iam.PolicyStatement({
|
|
571
|
+
actions: [
|
|
572
|
+
'bedrock-agentcore:GetPaymentInstrument',
|
|
573
|
+
'bedrock-agentcore:ListPaymentInstruments',
|
|
574
|
+
'bedrock-agentcore:GetPaymentInstrumentBalance',
|
|
575
|
+
'bedrock-agentcore:GetPaymentSession',
|
|
576
|
+
'bedrock-agentcore:ListPaymentSessions',
|
|
577
|
+
'bedrock-agentcore:CreatePaymentSession',
|
|
578
|
+
'bedrock-agentcore:ProcessPayment',
|
|
579
|
+
],
|
|
580
|
+
resources: [manager.paymentManagerArn, \`\${manager.paymentManagerArn}/*\`],
|
|
581
|
+
})
|
|
582
|
+
);
|
|
583
|
+
|
|
584
|
+
if (payment.autoPayment !== undefined) {
|
|
585
|
+
env.runtime.addEnvironmentVariable(\`\${prefix}_AUTO_PAYMENT\`, String(payment.autoPayment));
|
|
586
|
+
}
|
|
587
|
+
if (payment.paymentToolAllowlist) {
|
|
588
|
+
env.runtime.addEnvironmentVariable(\`\${prefix}_TOOL_ALLOWLIST\`, payment.paymentToolAllowlist.join(','));
|
|
589
|
+
}
|
|
590
|
+
if (payment.networkPreferences) {
|
|
591
|
+
env.runtime.addEnvironmentVariable(\`\${prefix}_NETWORK_PREFERENCES\`, payment.networkPreferences.join(','));
|
|
592
|
+
}
|
|
593
|
+
if (payment.authorizerType === 'CUSTOM_JWT') {
|
|
594
|
+
env.runtime.addEnvironmentVariable(\`\${prefix}_AUTH_MODE\`, 'bearer');
|
|
595
|
+
}
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
// Create connectors for this manager
|
|
599
|
+
for (const connector of payment.connectors) {
|
|
600
|
+
const connId = toCdkId(connector.name);
|
|
601
|
+
const schemaConnector =
|
|
602
|
+
connector.provisionMode === 'QUICK_CREATE'
|
|
603
|
+
? connector
|
|
604
|
+
: {
|
|
605
|
+
name: connector.name,
|
|
606
|
+
provider: connector.provider,
|
|
607
|
+
...(connector.provisionMode && { provisionMode: connector.provisionMode }),
|
|
608
|
+
credentialName: connector.credentialName,
|
|
609
|
+
};
|
|
610
|
+
const compatibilityProps = {
|
|
611
|
+
projectName: spec.name,
|
|
612
|
+
paymentManager: manager,
|
|
613
|
+
connector: schemaConnector,
|
|
614
|
+
// Remove these legacy manual fields after the new L3 release is pinned.
|
|
615
|
+
connectorName: connector.name,
|
|
616
|
+
connectorType: connector.provider,
|
|
617
|
+
...(connector.provisionMode !== 'QUICK_CREATE' && {
|
|
618
|
+
credentialProviderArn: connector.credentialProviderArn,
|
|
619
|
+
}),
|
|
620
|
+
};
|
|
621
|
+
const conn = new AgentCorePaymentConnector(
|
|
622
|
+
this,
|
|
623
|
+
\`Payment\${mgrId}\${connId}\`,
|
|
624
|
+
compatibilityProps as unknown as ConstructorParameters<typeof AgentCorePaymentConnector>[2]
|
|
625
|
+
);
|
|
626
|
+
|
|
627
|
+
// Wire first connector's ID as env var (eligible agents only)
|
|
628
|
+
if (connector === payment.connectors[0]) {
|
|
629
|
+
for (const env of this.application.environments.values()) {
|
|
630
|
+
if (!isPaymentEligibleAgent(env.agent)) continue;
|
|
631
|
+
env.runtime.addEnvironmentVariable(\`\${prefix}_CONNECTOR_ID\`, conn.paymentConnectorId);
|
|
632
|
+
}
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
new CfnOutput(this, \`Payment\${mgrId}\${connId}ConnectorId\`, {
|
|
636
|
+
value: conn.paymentConnectorId,
|
|
637
|
+
});
|
|
638
|
+
if (connector.provisionMode === 'QUICK_CREATE') {
|
|
639
|
+
const quickCreateConnector = conn as AgentCorePaymentConnector & {
|
|
640
|
+
paymentConnectorStatus: string;
|
|
641
|
+
authorizationUrl: string;
|
|
642
|
+
};
|
|
643
|
+
new CfnOutput(this, \`Payment\${mgrId}\${connId}ConnectorStatus\`, {
|
|
644
|
+
value: quickCreateConnector.paymentConnectorStatus,
|
|
645
|
+
});
|
|
646
|
+
new CfnOutput(this, \`Payment\${mgrId}\${connId}AuthorizationUrl\`, {
|
|
647
|
+
value: quickCreateConnector.authorizationUrl,
|
|
648
|
+
});
|
|
649
|
+
}
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
// CFN Outputs for post-deploy state parsing
|
|
653
|
+
new CfnOutput(this, \`Payment\${mgrId}ManagerArn\`, {
|
|
654
|
+
value: manager.paymentManagerArn,
|
|
655
|
+
});
|
|
656
|
+
new CfnOutput(this, \`Payment\${mgrId}ManagerId\`, {
|
|
657
|
+
value: manager.paymentManagerId,
|
|
658
|
+
});
|
|
659
|
+
new CfnOutput(this, \`Payment\${mgrId}ProcessPaymentRoleArn\`, {
|
|
660
|
+
value: manager.processPaymentRoleArn,
|
|
661
|
+
});
|
|
662
|
+
new CfnOutput(this, \`Payment\${mgrId}ResourceRetrievalRoleArn\`, {
|
|
663
|
+
value: manager.resourceRetrievalRoleArn,
|
|
664
|
+
});
|
|
665
|
+
}
|
|
666
|
+
}
|
|
667
|
+
|
|
375
668
|
// Stack-level output
|
|
376
669
|
new CfnOutput(this, 'StackNameOutput', {
|
|
377
670
|
description: 'Name of the CloudFormation Stack',
|
|
@@ -409,18 +702,18 @@ exports[`Assets Directory Snapshots > CDK assets > cdk/cdk/package.json should m
|
|
|
409
702
|
"format:check": "prettier --check ."
|
|
410
703
|
},
|
|
411
704
|
"devDependencies": {
|
|
412
|
-
"@types/jest": "
|
|
413
|
-
"@types/node": "
|
|
414
|
-
"jest": "
|
|
415
|
-
"ts-jest": "
|
|
416
|
-
"aws-cdk": "2.
|
|
417
|
-
"prettier": "
|
|
705
|
+
"@types/jest": "~29.5.14",
|
|
706
|
+
"@types/node": "~24.13.3",
|
|
707
|
+
"jest": "~29.7.0",
|
|
708
|
+
"ts-jest": "~29.4.11",
|
|
709
|
+
"aws-cdk": "~2.1126.0",
|
|
710
|
+
"prettier": "~3.9.5",
|
|
418
711
|
"typescript": "~5.9.3"
|
|
419
712
|
},
|
|
420
713
|
"dependencies": {
|
|
421
|
-
"@aws/agentcore-cdk": "
|
|
422
|
-
"aws-cdk-lib": "
|
|
423
|
-
"constructs": "
|
|
714
|
+
"@aws/agentcore-cdk": "0.1.0-alpha.53",
|
|
715
|
+
"aws-cdk-lib": "~2.261.0",
|
|
716
|
+
"constructs": "~10.7.0"
|
|
424
717
|
}
|
|
425
718
|
}
|
|
426
719
|
"
|
|
@@ -443,10 +736,14 @@ test('AgentCoreStack synthesizes with empty spec', () => {
|
|
|
443
736
|
credentials: [],
|
|
444
737
|
evaluators: [],
|
|
445
738
|
onlineEvalConfigs: [],
|
|
739
|
+
configBundles: [],
|
|
446
740
|
policyEngines: [],
|
|
741
|
+
payments: [],
|
|
447
742
|
agentCoreGateways: [],
|
|
448
743
|
mcpRuntimeTools: [],
|
|
449
744
|
unassignedTargets: [],
|
|
745
|
+
datasets: [],
|
|
746
|
+
knowledgeBases: [],
|
|
450
747
|
},
|
|
451
748
|
});
|
|
452
749
|
const template = Template.fromStack(stack);
|
|
@@ -507,6 +804,16 @@ exports[`Assets Directory Snapshots > File listing > should match the expected f
|
|
|
507
804
|
"cdk/tsconfig.json",
|
|
508
805
|
"container/python/Dockerfile",
|
|
509
806
|
"container/python/dockerignore.template",
|
|
807
|
+
"container/typescript/Dockerfile",
|
|
808
|
+
"container/typescript/dockerignore.template",
|
|
809
|
+
"datasets/predefined-v1.jsonl",
|
|
810
|
+
"datasets/simulated-v1.jsonl",
|
|
811
|
+
"evaluators/autoevals-lambda/execution-role-policy.json",
|
|
812
|
+
"evaluators/autoevals-lambda/lambda_function.py",
|
|
813
|
+
"evaluators/autoevals-lambda/pyproject.toml",
|
|
814
|
+
"evaluators/deepeval-lambda/execution-role-policy.json",
|
|
815
|
+
"evaluators/deepeval-lambda/lambda_function.py",
|
|
816
|
+
"evaluators/deepeval-lambda/pyproject.toml",
|
|
510
817
|
"evaluators/python-lambda/execution-role-policy.json",
|
|
511
818
|
"evaluators/python-lambda/lambda_function.py",
|
|
512
819
|
"evaluators/python-lambda/pyproject.toml",
|
|
@@ -596,14 +903,33 @@ exports[`Assets Directory Snapshots > File listing > should match the expected f
|
|
|
596
903
|
"python/http/strands/base/mcp_client/client.py",
|
|
597
904
|
"python/http/strands/base/model/__init__.py",
|
|
598
905
|
"python/http/strands/base/model/load.py",
|
|
906
|
+
"python/http/strands/base/model/mantle_compat.py",
|
|
599
907
|
"python/http/strands/base/pyproject.toml",
|
|
908
|
+
"python/http/strands/base/skills/fetcher.py",
|
|
909
|
+
"python/http/strands/capabilities/execution-limits/hooks/execution_limits.py",
|
|
600
910
|
"python/http/strands/capabilities/memory/__init__.py",
|
|
601
911
|
"python/http/strands/capabilities/memory/session.py",
|
|
912
|
+
"python/http/strands/capabilities/payments/__init__.py",
|
|
913
|
+
"python/http/strands/capabilities/payments/payments.py",
|
|
602
914
|
"python/mcp/standalone/base/README.md",
|
|
603
915
|
"python/mcp/standalone/base/gitignore.template",
|
|
604
916
|
"python/mcp/standalone/base/main.py",
|
|
605
917
|
"python/mcp/standalone/base/pyproject.toml",
|
|
606
918
|
"typescript/.gitkeep",
|
|
919
|
+
"typescript/http/strands/base/README.md",
|
|
920
|
+
"typescript/http/strands/base/gitignore.template",
|
|
921
|
+
"typescript/http/strands/base/main.ts",
|
|
922
|
+
"typescript/http/strands/base/mcp_client/client.ts",
|
|
923
|
+
"typescript/http/strands/base/model/load.ts",
|
|
924
|
+
"typescript/http/strands/base/package.json",
|
|
925
|
+
"typescript/http/strands/base/tsconfig.json",
|
|
926
|
+
"typescript/http/strands/capabilities/memory/memory.ts",
|
|
927
|
+
"typescript/http/vercelai/base/README.md",
|
|
928
|
+
"typescript/http/vercelai/base/gitignore.template",
|
|
929
|
+
"typescript/http/vercelai/base/main.ts",
|
|
930
|
+
"typescript/http/vercelai/base/model/load.ts",
|
|
931
|
+
"typescript/http/vercelai/base/package.json",
|
|
932
|
+
"typescript/http/vercelai/base/tsconfig.json",
|
|
607
933
|
]
|
|
608
934
|
`;
|
|
609
935
|
|
|
@@ -650,7 +976,7 @@ description = "MCP Server demonstrating HTTP tool patterns"
|
|
|
650
976
|
readme = "README.md"
|
|
651
977
|
requires-python = ">=3.10"
|
|
652
978
|
dependencies = [
|
|
653
|
-
"mcp[cli]
|
|
979
|
+
"mcp[cli] ~= 1.24.0",
|
|
654
980
|
"httpx >= 0.27.0",
|
|
655
981
|
"opentelemetry-distro",
|
|
656
982
|
"opentelemetry-exporter-otlp",
|
|
@@ -1050,12 +1376,14 @@ Thumbs.db
|
|
|
1050
1376
|
`;
|
|
1051
1377
|
|
|
1052
1378
|
exports[`Assets Directory Snapshots > Python framework assets > python/python/a2a/googleadk/base/main.py should match snapshot 1`] = `
|
|
1053
|
-
"
|
|
1379
|
+
"{{#if needsOs}}
|
|
1380
|
+
import os
|
|
1381
|
+
{{/if}}
|
|
1054
1382
|
from google.adk.agents import Agent
|
|
1055
1383
|
from google.adk.a2a.executor.a2a_agent_executor import A2aAgentExecutor
|
|
1056
1384
|
from google.adk.runners import Runner
|
|
1057
1385
|
from google.adk.sessions import InMemorySessionService
|
|
1058
|
-
from a2a.types import AgentCapabilities, AgentCard, AgentSkill
|
|
1386
|
+
from a2a.types import AgentCapabilities, AgentCard, AgentInterface, AgentSkill
|
|
1059
1387
|
from bedrock_agentcore.runtime import serve_a2a
|
|
1060
1388
|
from model.load import load_model
|
|
1061
1389
|
|
|
@@ -1069,18 +1397,21 @@ def add_numbers(a: int, b: int) -> int:
|
|
|
1069
1397
|
|
|
1070
1398
|
tools = [add_numbers]
|
|
1071
1399
|
|
|
1072
|
-
{{#if
|
|
1073
|
-
|
|
1400
|
+
{{#if needsOs}}
|
|
1401
|
+
_MOUNT_PATHS = [
|
|
1402
|
+
{{#if sessionStorageMountPath}}"{{sessionStorageMountPath}}",{{/if}}
|
|
1403
|
+
{{#each efsMounts}}"{{mountPath}}",{{/each}}
|
|
1404
|
+
{{#each s3Mounts}}"{{mountPath}}",{{/each}}
|
|
1405
|
+
]
|
|
1074
1406
|
|
|
1075
1407
|
def _safe_resolve(path: str) -> str:
|
|
1076
|
-
|
|
1077
|
-
resolved
|
|
1078
|
-
|
|
1079
|
-
raise ValueError(f"Path '{path}' is outside the storage boundary")
|
|
1408
|
+
resolved = os.path.realpath(path)
|
|
1409
|
+
if not any(resolved == os.path.realpath(m) or resolved.startswith(os.path.realpath(m) + os.sep) for m in _MOUNT_PATHS):
|
|
1410
|
+
raise ValueError(f"Path '{path}' is not within any configured mount ({', '.join(_MOUNT_PATHS)})")
|
|
1080
1411
|
return resolved
|
|
1081
1412
|
|
|
1082
1413
|
def file_read(path: str) -> str:
|
|
1083
|
-
"""Read a file from
|
|
1414
|
+
"""Read a file from a mounted filesystem. Use the absolute path (e.g. /mnt/tools/data.txt)."""
|
|
1084
1415
|
try:
|
|
1085
1416
|
full_path = _safe_resolve(path)
|
|
1086
1417
|
with open(full_path) as f:
|
|
@@ -1091,7 +1422,7 @@ def file_read(path: str) -> str:
|
|
|
1091
1422
|
return f"Error reading '{path}': {e.strerror}"
|
|
1092
1423
|
|
|
1093
1424
|
def file_write(path: str, content: str) -> str:
|
|
1094
|
-
"""Write
|
|
1425
|
+
"""Write a file to a mounted filesystem. Use the absolute path (e.g. /mnt/tools/data.txt)."""
|
|
1095
1426
|
try:
|
|
1096
1427
|
full_path = _safe_resolve(path)
|
|
1097
1428
|
parent = os.path.dirname(full_path)
|
|
@@ -1105,25 +1436,28 @@ def file_write(path: str, content: str) -> str:
|
|
|
1105
1436
|
except OSError as e:
|
|
1106
1437
|
return f"Error writing '{path}': {e.strerror}"
|
|
1107
1438
|
|
|
1108
|
-
def list_files(
|
|
1109
|
-
"""List files in
|
|
1439
|
+
def list_files(path: str) -> str:
|
|
1440
|
+
"""List files in a mounted filesystem directory. Use the absolute path (e.g. /mnt/tools)."""
|
|
1110
1441
|
try:
|
|
1111
|
-
|
|
1112
|
-
entries = os.listdir(
|
|
1442
|
+
full_path = _safe_resolve(path)
|
|
1443
|
+
entries = os.listdir(full_path)
|
|
1113
1444
|
return "\\n".join(entries) if entries else "(empty directory)"
|
|
1114
1445
|
except ValueError as e:
|
|
1115
1446
|
return str(e)
|
|
1116
1447
|
except OSError as e:
|
|
1117
|
-
return f"Error listing '{
|
|
1448
|
+
return f"Error listing '{path}': {e.strerror}"
|
|
1118
1449
|
|
|
1119
1450
|
tools.extend([file_read, file_write, list_files])
|
|
1120
1451
|
{{/if}}
|
|
1121
1452
|
|
|
1122
1453
|
AGENT_INSTRUCTION = """
|
|
1123
1454
|
You are a helpful assistant. Use tools when appropriate.
|
|
1124
|
-
{{#if
|
|
1125
|
-
You have
|
|
1126
|
-
{{
|
|
1455
|
+
{{#if needsOs}}
|
|
1456
|
+
You have access to the following mounted filesystems. Use file_read, file_write, and list_files with full absolute paths:
|
|
1457
|
+
{{#if sessionStorageMountPath}}- {{sessionStorageMountPath}}: ephemeral session storage (lost when session ends)
|
|
1458
|
+
{{/if}}{{#each efsMounts}}- {{mountPath}}: EFS persistent storage (persists across sessions and agent restarts)
|
|
1459
|
+
{{/each}}{{#each s3Mounts}}- {{mountPath}}: S3 Files persistent storage (durable, backed by S3)
|
|
1460
|
+
{{/each}}{{/if}}
|
|
1127
1461
|
"""
|
|
1128
1462
|
|
|
1129
1463
|
agent = Agent(
|
|
@@ -1143,7 +1477,6 @@ runner = Runner(
|
|
|
1143
1477
|
card = AgentCard(
|
|
1144
1478
|
name=agent.name,
|
|
1145
1479
|
description=agent.description,
|
|
1146
|
-
url="http://localhost:9000/",
|
|
1147
1480
|
version="0.1.0",
|
|
1148
1481
|
capabilities=AgentCapabilities(streaming=True),
|
|
1149
1482
|
skills=[
|
|
@@ -1156,6 +1489,13 @@ card = AgentCard(
|
|
|
1156
1489
|
],
|
|
1157
1490
|
default_input_modes=["text"],
|
|
1158
1491
|
default_output_modes=["text"],
|
|
1492
|
+
supported_interfaces=[
|
|
1493
|
+
AgentInterface(
|
|
1494
|
+
protocol_binding="JSONRPC",
|
|
1495
|
+
protocol_version="1.0",
|
|
1496
|
+
url="http://localhost:9000/",
|
|
1497
|
+
)
|
|
1498
|
+
],
|
|
1159
1499
|
)
|
|
1160
1500
|
|
|
1161
1501
|
if __name__ == "__main__":
|
|
@@ -1225,11 +1565,13 @@ description = "AgentCore A2A Agent using Google ADK"
|
|
|
1225
1565
|
readme = "README.md"
|
|
1226
1566
|
requires-python = ">=3.10"
|
|
1227
1567
|
dependencies = [
|
|
1228
|
-
"a2a-sdk >= 0.2.0",
|
|
1568
|
+
"a2a-sdk[http-server] >= 1.0.1, < 2.0.0",
|
|
1229
1569
|
"aws-opentelemetry-distro",
|
|
1230
|
-
"bedrock-agentcore[a2a] >= 1.0
|
|
1231
|
-
"google-adk >=
|
|
1232
|
-
"google-genai >=
|
|
1570
|
+
"bedrock-agentcore[a2a-v1] >= 1.19.0",
|
|
1571
|
+
"google-adk[a2a] >= 2.5.0, < 3.0.0",
|
|
1572
|
+
"google-genai >= 2.9.0, < 3.0.0",
|
|
1573
|
+
# 1.13.0 was yanked for broken imports.
|
|
1574
|
+
"opentelemetry-resourcedetector-gcp >= 1.9.0a0, < 2.0.0, != 1.13.0",
|
|
1233
1575
|
]
|
|
1234
1576
|
|
|
1235
1577
|
[tool.hatch.build.targets.wheel]
|
|
@@ -1309,15 +1651,17 @@ Thumbs.db
|
|
|
1309
1651
|
`;
|
|
1310
1652
|
|
|
1311
1653
|
exports[`Assets Directory Snapshots > Python framework assets > python/python/a2a/langchain_langgraph/base/main.py should match snapshot 1`] = `
|
|
1312
|
-
"
|
|
1654
|
+
"{{#if needsOs}}
|
|
1655
|
+
import os
|
|
1656
|
+
{{/if}}
|
|
1313
1657
|
from langchain_core.tools import tool
|
|
1314
1658
|
from langgraph.prebuilt import create_react_agent
|
|
1315
1659
|
from opentelemetry.instrumentation.langchain import LangchainInstrumentor
|
|
1660
|
+
from a2a.helpers import new_task_from_user_message
|
|
1316
1661
|
from a2a.server.agent_execution import AgentExecutor, RequestContext
|
|
1317
1662
|
from a2a.server.events import EventQueue
|
|
1318
1663
|
from a2a.server.tasks import TaskUpdater
|
|
1319
|
-
from a2a.types import AgentCapabilities, AgentCard, AgentSkill, Part
|
|
1320
|
-
from a2a.utils import new_task
|
|
1664
|
+
from a2a.types import AgentCapabilities, AgentCard, AgentInterface, AgentSkill, Part
|
|
1321
1665
|
from bedrock_agentcore.runtime import serve_a2a
|
|
1322
1666
|
from model.load import load_model
|
|
1323
1667
|
|
|
@@ -1332,19 +1676,22 @@ def add_numbers(a: int, b: int) -> int:
|
|
|
1332
1676
|
|
|
1333
1677
|
tools = [add_numbers]
|
|
1334
1678
|
|
|
1335
|
-
{{#if
|
|
1336
|
-
|
|
1679
|
+
{{#if needsOs}}
|
|
1680
|
+
_MOUNT_PATHS = [
|
|
1681
|
+
{{#if sessionStorageMountPath}}"{{sessionStorageMountPath}}",{{/if}}
|
|
1682
|
+
{{#each efsMounts}}"{{mountPath}}",{{/each}}
|
|
1683
|
+
{{#each s3Mounts}}"{{mountPath}}",{{/each}}
|
|
1684
|
+
]
|
|
1337
1685
|
|
|
1338
1686
|
def _safe_resolve(path: str) -> str:
|
|
1339
|
-
|
|
1340
|
-
resolved
|
|
1341
|
-
|
|
1342
|
-
raise ValueError(f"Path '{path}' is outside the storage boundary")
|
|
1687
|
+
resolved = os.path.realpath(path)
|
|
1688
|
+
if not any(resolved == os.path.realpath(m) or resolved.startswith(os.path.realpath(m) + os.sep) for m in _MOUNT_PATHS):
|
|
1689
|
+
raise ValueError(f"Path '{path}' is not within any configured mount ({', '.join(_MOUNT_PATHS)})")
|
|
1343
1690
|
return resolved
|
|
1344
1691
|
|
|
1345
1692
|
@tool
|
|
1346
1693
|
def file_read(path: str) -> str:
|
|
1347
|
-
"""Read a file from
|
|
1694
|
+
"""Read a file from a mounted filesystem. Use the absolute path (e.g. /mnt/tools/data.txt)."""
|
|
1348
1695
|
try:
|
|
1349
1696
|
full_path = _safe_resolve(path)
|
|
1350
1697
|
with open(full_path) as f:
|
|
@@ -1356,7 +1703,7 @@ def file_read(path: str) -> str:
|
|
|
1356
1703
|
|
|
1357
1704
|
@tool
|
|
1358
1705
|
def file_write(path: str, content: str) -> str:
|
|
1359
|
-
"""Write
|
|
1706
|
+
"""Write a file to a mounted filesystem. Use the absolute path (e.g. /mnt/tools/data.txt)."""
|
|
1360
1707
|
try:
|
|
1361
1708
|
full_path = _safe_resolve(path)
|
|
1362
1709
|
parent = os.path.dirname(full_path)
|
|
@@ -1371,25 +1718,28 @@ def file_write(path: str, content: str) -> str:
|
|
|
1371
1718
|
return f"Error writing '{path}': {e.strerror}"
|
|
1372
1719
|
|
|
1373
1720
|
@tool
|
|
1374
|
-
def list_files(
|
|
1375
|
-
"""List files in
|
|
1721
|
+
def list_files(path: str) -> str:
|
|
1722
|
+
"""List files in a mounted filesystem directory. Use the absolute path (e.g. /mnt/tools)."""
|
|
1376
1723
|
try:
|
|
1377
|
-
|
|
1378
|
-
entries = os.listdir(
|
|
1724
|
+
full_path = _safe_resolve(path)
|
|
1725
|
+
entries = os.listdir(full_path)
|
|
1379
1726
|
return "\\n".join(entries) if entries else "(empty directory)"
|
|
1380
1727
|
except ValueError as e:
|
|
1381
1728
|
return str(e)
|
|
1382
1729
|
except OSError as e:
|
|
1383
|
-
return f"Error listing '{
|
|
1730
|
+
return f"Error listing '{path}': {e.strerror}"
|
|
1384
1731
|
|
|
1385
1732
|
tools.extend([file_read, file_write, list_files])
|
|
1386
1733
|
{{/if}}
|
|
1387
1734
|
|
|
1388
1735
|
SYSTEM_PROMPT = """
|
|
1389
1736
|
You are a helpful assistant. Use tools when appropriate.
|
|
1390
|
-
{{#if
|
|
1391
|
-
You have
|
|
1392
|
-
{{
|
|
1737
|
+
{{#if needsOs}}
|
|
1738
|
+
You have access to the following mounted filesystems. Use file_read, file_write, and list_files with full absolute paths:
|
|
1739
|
+
{{#if sessionStorageMountPath}}- {{sessionStorageMountPath}}: ephemeral session storage (lost when session ends)
|
|
1740
|
+
{{/if}}{{#each efsMounts}}- {{mountPath}}: EFS persistent storage (persists across sessions and agent restarts)
|
|
1741
|
+
{{/each}}{{#each s3Mounts}}- {{mountPath}}: S3 Files persistent storage (durable, backed by S3)
|
|
1742
|
+
{{/each}}{{/if}}
|
|
1393
1743
|
"""
|
|
1394
1744
|
|
|
1395
1745
|
model = load_model()
|
|
@@ -1403,7 +1753,7 @@ class LangGraphA2AExecutor(AgentExecutor):
|
|
|
1403
1753
|
self.graph = graph
|
|
1404
1754
|
|
|
1405
1755
|
async def execute(self, context: RequestContext, event_queue: EventQueue) -> None:
|
|
1406
|
-
task = context.current_task or
|
|
1756
|
+
task = context.current_task or new_task_from_user_message(context.message)
|
|
1407
1757
|
if not context.current_task:
|
|
1408
1758
|
await event_queue.enqueue_event(task)
|
|
1409
1759
|
updater = TaskUpdater(event_queue, task.id, task.context_id)
|
|
@@ -1412,7 +1762,7 @@ class LangGraphA2AExecutor(AgentExecutor):
|
|
|
1412
1762
|
result = await self.graph.ainvoke({"messages": [("user", user_text)]})
|
|
1413
1763
|
response = result["messages"][-1].content
|
|
1414
1764
|
|
|
1415
|
-
await updater.add_artifact([Part(
|
|
1765
|
+
await updater.add_artifact([Part(text=response)])
|
|
1416
1766
|
await updater.complete()
|
|
1417
1767
|
|
|
1418
1768
|
async def cancel(self, context: RequestContext, event_queue: EventQueue) -> None:
|
|
@@ -1422,7 +1772,6 @@ class LangGraphA2AExecutor(AgentExecutor):
|
|
|
1422
1772
|
card = AgentCard(
|
|
1423
1773
|
name="{{ name }}",
|
|
1424
1774
|
description="A LangGraph agent on Bedrock AgentCore",
|
|
1425
|
-
url="http://localhost:9000/",
|
|
1426
1775
|
version="0.1.0",
|
|
1427
1776
|
capabilities=AgentCapabilities(streaming=True),
|
|
1428
1777
|
skills=[
|
|
@@ -1435,6 +1784,13 @@ card = AgentCard(
|
|
|
1435
1784
|
],
|
|
1436
1785
|
default_input_modes=["text"],
|
|
1437
1786
|
default_output_modes=["text"],
|
|
1787
|
+
supported_interfaces=[
|
|
1788
|
+
AgentInterface(
|
|
1789
|
+
protocol_binding="JSONRPC",
|
|
1790
|
+
protocol_version="1.0",
|
|
1791
|
+
url="http://localhost:9000/",
|
|
1792
|
+
)
|
|
1793
|
+
],
|
|
1438
1794
|
)
|
|
1439
1795
|
|
|
1440
1796
|
if __name__ == "__main__":
|
|
@@ -1586,14 +1942,15 @@ description = "AgentCore A2A Agent using LangChain + LangGraph"
|
|
|
1586
1942
|
readme = "README.md"
|
|
1587
1943
|
requires-python = ">=3.10"
|
|
1588
1944
|
dependencies = [
|
|
1589
|
-
"a2a-sdk >= 0.2.0",
|
|
1945
|
+
"a2a-sdk[http-server] >= 1.0.1, < 2.0.0",
|
|
1590
1946
|
{{#if (eq modelProvider "Anthropic")}}"langchain-anthropic >= 0.3.0",
|
|
1591
1947
|
{{/if}}{{#if (eq modelProvider "Bedrock")}}"langchain-aws >= 0.2.0",
|
|
1592
1948
|
{{/if}}{{#if (eq modelProvider "Gemini")}}"langchain-google-genai >= 2.0.0",
|
|
1949
|
+
"google-genai >= 1.0.0, < 2.0.0",
|
|
1593
1950
|
{{/if}}{{#if (eq modelProvider "OpenAI")}}"langchain-openai >= 0.2.0",
|
|
1594
|
-
{{/if}}"aws-opentelemetry-distro",
|
|
1951
|
+
{{/if}}"aws-opentelemetry-distro ~= 0.18.0",
|
|
1595
1952
|
"opentelemetry-instrumentation-langchain >= 0.59.0",
|
|
1596
|
-
"bedrock-agentcore[a2a] >= 1.0
|
|
1953
|
+
"bedrock-agentcore[a2a-v1] >= 1.19.0",
|
|
1597
1954
|
"botocore[crt] >= 1.35.0",
|
|
1598
1955
|
"langgraph >= 0.2.0",
|
|
1599
1956
|
]
|
|
@@ -1682,7 +2039,7 @@ from model.load import load_model
|
|
|
1682
2039
|
{{#if hasMemory}}
|
|
1683
2040
|
from memory.session import get_memory_session_manager
|
|
1684
2041
|
{{/if}}
|
|
1685
|
-
{{#if
|
|
2042
|
+
{{#if needsOs}}
|
|
1686
2043
|
import os
|
|
1687
2044
|
{{/if}}
|
|
1688
2045
|
|
|
@@ -1695,19 +2052,22 @@ def add_numbers(a: int, b: int) -> int:
|
|
|
1695
2052
|
|
|
1696
2053
|
tools = [add_numbers]
|
|
1697
2054
|
|
|
1698
|
-
{{#if
|
|
1699
|
-
|
|
2055
|
+
{{#if needsOs}}
|
|
2056
|
+
_MOUNT_PATHS = [
|
|
2057
|
+
{{#if sessionStorageMountPath}}"{{sessionStorageMountPath}}",{{/if}}
|
|
2058
|
+
{{#each efsMounts}}"{{mountPath}}",{{/each}}
|
|
2059
|
+
{{#each s3Mounts}}"{{mountPath}}",{{/each}}
|
|
2060
|
+
]
|
|
1700
2061
|
|
|
1701
2062
|
def _safe_resolve(path: str) -> str:
|
|
1702
|
-
|
|
1703
|
-
resolved
|
|
1704
|
-
|
|
1705
|
-
raise ValueError(f"Path '{path}' is outside the storage boundary")
|
|
2063
|
+
resolved = os.path.realpath(path)
|
|
2064
|
+
if not any(resolved == os.path.realpath(m) or resolved.startswith(os.path.realpath(m) + os.sep) for m in _MOUNT_PATHS):
|
|
2065
|
+
raise ValueError(f"Path '{path}' is not within any configured mount ({', '.join(_MOUNT_PATHS)})")
|
|
1706
2066
|
return resolved
|
|
1707
2067
|
|
|
1708
2068
|
@tool
|
|
1709
2069
|
def file_read(path: str) -> str:
|
|
1710
|
-
"""Read a file from
|
|
2070
|
+
"""Read a file from a mounted filesystem. Use the absolute path (e.g. /mnt/tools/data.txt)."""
|
|
1711
2071
|
try:
|
|
1712
2072
|
full_path = _safe_resolve(path)
|
|
1713
2073
|
with open(full_path) as f:
|
|
@@ -1719,7 +2079,7 @@ def file_read(path: str) -> str:
|
|
|
1719
2079
|
|
|
1720
2080
|
@tool
|
|
1721
2081
|
def file_write(path: str, content: str) -> str:
|
|
1722
|
-
"""Write
|
|
2082
|
+
"""Write a file to a mounted filesystem. Use the absolute path (e.g. /mnt/tools/data.txt)."""
|
|
1723
2083
|
try:
|
|
1724
2084
|
full_path = _safe_resolve(path)
|
|
1725
2085
|
parent = os.path.dirname(full_path)
|
|
@@ -1734,25 +2094,28 @@ def file_write(path: str, content: str) -> str:
|
|
|
1734
2094
|
return f"Error writing '{path}': {e.strerror}"
|
|
1735
2095
|
|
|
1736
2096
|
@tool
|
|
1737
|
-
def list_files(
|
|
1738
|
-
"""List files in
|
|
2097
|
+
def list_files(path: str) -> str:
|
|
2098
|
+
"""List files in a mounted filesystem directory. Use the absolute path (e.g. /mnt/tools)."""
|
|
1739
2099
|
try:
|
|
1740
|
-
|
|
1741
|
-
entries = os.listdir(
|
|
2100
|
+
full_path = _safe_resolve(path)
|
|
2101
|
+
entries = os.listdir(full_path)
|
|
1742
2102
|
return "\\n".join(entries) if entries else "(empty directory)"
|
|
1743
2103
|
except ValueError as e:
|
|
1744
2104
|
return str(e)
|
|
1745
2105
|
except OSError as e:
|
|
1746
|
-
return f"Error listing '{
|
|
2106
|
+
return f"Error listing '{path}': {e.strerror}"
|
|
1747
2107
|
|
|
1748
2108
|
tools.extend([file_read, file_write, list_files])
|
|
1749
2109
|
{{/if}}
|
|
1750
2110
|
|
|
1751
2111
|
SYSTEM_PROMPT = """
|
|
1752
2112
|
You are a helpful assistant. Use tools when appropriate.
|
|
1753
|
-
{{#if
|
|
1754
|
-
You have
|
|
1755
|
-
{{
|
|
2113
|
+
{{#if needsOs}}
|
|
2114
|
+
You have access to the following mounted filesystems. Use file_read, file_write, and list_files with full absolute paths:
|
|
2115
|
+
{{#if sessionStorageMountPath}}- {{sessionStorageMountPath}}: ephemeral session storage (lost when session ends)
|
|
2116
|
+
{{/if}}{{#each efsMounts}}- {{mountPath}}: EFS persistent storage (persists across sessions and agent restarts)
|
|
2117
|
+
{{/each}}{{#each s3Mounts}}- {{mountPath}}: S3 Files persistent storage (durable, backed by S3)
|
|
2118
|
+
{{/each}}{{/if}}
|
|
1756
2119
|
"""
|
|
1757
2120
|
|
|
1758
2121
|
{{#if hasMemory}}
|
|
@@ -1930,9 +2293,9 @@ readme = "README.md"
|
|
|
1930
2293
|
requires-python = ">=3.10"
|
|
1931
2294
|
dependencies = [
|
|
1932
2295
|
{{#if (eq modelProvider "Anthropic")}}"anthropic >= 0.30.0",
|
|
1933
|
-
{{/if}}"a2a-sdk[all] >= 0.
|
|
1934
|
-
"aws-opentelemetry-distro",
|
|
1935
|
-
"bedrock-agentcore[a2a] >= 1.
|
|
2296
|
+
{{/if}}"a2a-sdk[all] >= 0.3.0, < 0.4.0",
|
|
2297
|
+
"aws-opentelemetry-distro ~= 0.18.0",
|
|
2298
|
+
"bedrock-agentcore[a2a] >= 1.9.1",
|
|
1936
2299
|
"botocore[crt] >= 1.35.0",
|
|
1937
2300
|
{{#if (eq modelProvider "Gemini")}}"google-genai >= 1.0.0",
|
|
1938
2301
|
{{/if}}{{#if (eq modelProvider "OpenAI")}}"openai >= 1.0.0",
|
|
@@ -1976,8 +2339,11 @@ def get_memory_session_manager(session_id: Optional[str], actor_id: str) -> Opti
|
|
|
1976
2339
|
{{#if (includes memoryProviders.[0].strategies "USER_PREFERENCE")}}
|
|
1977
2340
|
f"/users/{actor_id}/preferences": RetrievalConfig(top_k=3, relevance_score=0.5),
|
|
1978
2341
|
{{/if}}
|
|
2342
|
+
{{#if (includes memoryProviders.[0].strategies "EPISODIC")}}
|
|
2343
|
+
f"/episodes/{actor_id}/{session_id}": RetrievalConfig(top_k=5, relevance_score=0.5),
|
|
2344
|
+
{{/if}}
|
|
1979
2345
|
{{#if (includes memoryProviders.[0].strategies "SUMMARIZATION")}}
|
|
1980
|
-
f"/summaries/{actor_id}
|
|
2346
|
+
f"/summaries/{actor_id}": RetrievalConfig(top_k=3, relevance_score=0.5),
|
|
1981
2347
|
{{/if}}
|
|
1982
2348
|
}
|
|
1983
2349
|
{{/if}}
|
|
@@ -2079,16 +2445,76 @@ exports[`Assets Directory Snapshots > Python framework assets > python/python/ag
|
|
|
2079
2445
|
"import os
|
|
2080
2446
|
import uvicorn
|
|
2081
2447
|
from google.adk.agents import LlmAgent
|
|
2448
|
+
from google.adk.tools import FunctionTool
|
|
2082
2449
|
from ag_ui_adk import ADKAgent, AGUIToolset, create_adk_app
|
|
2083
2450
|
from model.load import load_model
|
|
2084
2451
|
|
|
2085
2452
|
load_model()
|
|
2086
2453
|
|
|
2454
|
+
{{#if needsOs}}
|
|
2455
|
+
_MOUNT_PATHS = [
|
|
2456
|
+
{{#if sessionStorageMountPath}}"{{sessionStorageMountPath}}",{{/if}}
|
|
2457
|
+
{{#each efsMounts}}"{{mountPath}}",{{/each}}
|
|
2458
|
+
{{#each s3Mounts}}"{{mountPath}}",{{/each}}
|
|
2459
|
+
]
|
|
2460
|
+
|
|
2461
|
+
def _safe_resolve(path: str) -> str:
|
|
2462
|
+
resolved = os.path.realpath(path)
|
|
2463
|
+
if not any(resolved == os.path.realpath(m) or resolved.startswith(os.path.realpath(m) + os.sep) for m in _MOUNT_PATHS):
|
|
2464
|
+
raise ValueError(f"Path '{path}' is not within any configured mount ({', '.join(_MOUNT_PATHS)})")
|
|
2465
|
+
return resolved
|
|
2466
|
+
|
|
2467
|
+
def file_read(path: str) -> str:
|
|
2468
|
+
"""Read a file from a mounted filesystem. Use the absolute path (e.g. /mnt/tools/data.txt)."""
|
|
2469
|
+
try:
|
|
2470
|
+
full_path = _safe_resolve(path)
|
|
2471
|
+
with open(full_path) as f:
|
|
2472
|
+
return f.read()
|
|
2473
|
+
except ValueError as e:
|
|
2474
|
+
return str(e)
|
|
2475
|
+
except OSError as e:
|
|
2476
|
+
return f"Error reading '{path}': {e.strerror}"
|
|
2477
|
+
|
|
2478
|
+
def file_write(path: str, content: str) -> str:
|
|
2479
|
+
"""Write a file to a mounted filesystem. Use the absolute path (e.g. /mnt/tools/data.txt)."""
|
|
2480
|
+
try:
|
|
2481
|
+
full_path = _safe_resolve(path)
|
|
2482
|
+
parent = os.path.dirname(full_path)
|
|
2483
|
+
if parent:
|
|
2484
|
+
os.makedirs(parent, exist_ok=True)
|
|
2485
|
+
with open(full_path, "w") as f:
|
|
2486
|
+
f.write(content)
|
|
2487
|
+
return f"Written to {path}"
|
|
2488
|
+
except ValueError as e:
|
|
2489
|
+
return str(e)
|
|
2490
|
+
except OSError as e:
|
|
2491
|
+
return f"Error writing '{path}': {e.strerror}"
|
|
2492
|
+
|
|
2493
|
+
def list_files(path: str) -> str:
|
|
2494
|
+
"""List files in a mounted filesystem directory. Use the absolute path (e.g. /mnt/tools)."""
|
|
2495
|
+
try:
|
|
2496
|
+
full_path = _safe_resolve(path)
|
|
2497
|
+
entries = os.listdir(full_path)
|
|
2498
|
+
return "\\n".join(entries) if entries else "(empty directory)"
|
|
2499
|
+
except ValueError as e:
|
|
2500
|
+
return str(e)
|
|
2501
|
+
except OSError as e:
|
|
2502
|
+
return f"Error listing '{path}': {e.strerror}"
|
|
2503
|
+
|
|
2504
|
+
_fs_tools = [FunctionTool(file_read), FunctionTool(file_write), FunctionTool(list_files)]
|
|
2505
|
+
{{/if}}
|
|
2506
|
+
|
|
2087
2507
|
agent = LlmAgent(
|
|
2088
2508
|
name="{{ name }}",
|
|
2089
2509
|
model="gemini-2.5-flash",
|
|
2090
|
-
instruction="You are a helpful assistant.
|
|
2091
|
-
|
|
2510
|
+
instruction="""You are a helpful assistant.
|
|
2511
|
+
{{#if needsOs}}
|
|
2512
|
+
You have access to the following mounted filesystems. Use file_read, file_write, and list_files with full absolute paths:
|
|
2513
|
+
{{#if sessionStorageMountPath}}- {{sessionStorageMountPath}}: ephemeral session storage (lost when session ends)
|
|
2514
|
+
{{/if}}{{#each efsMounts}}- {{mountPath}}: EFS persistent storage (persists across sessions and agent restarts)
|
|
2515
|
+
{{/each}}{{#each s3Mounts}}- {{mountPath}}: S3 Files persistent storage (durable, backed by S3)
|
|
2516
|
+
{{/each}}{{/if}}""",
|
|
2517
|
+
tools=[AGUIToolset(), {{#if needsOs}}*_fs_tools{{/if}}],
|
|
2092
2518
|
)
|
|
2093
2519
|
|
|
2094
2520
|
adk_agent = ADKAgent(
|
|
@@ -2174,12 +2600,13 @@ requires-python = ">=3.10"
|
|
|
2174
2600
|
dependencies = [
|
|
2175
2601
|
"ag-ui-adk >= 0.6.0",
|
|
2176
2602
|
"ag-ui-protocol >= 0.1.10",
|
|
2177
|
-
"bedrock-agentcore >= 1.0
|
|
2603
|
+
"bedrock-agentcore >= 1.8.0",
|
|
2178
2604
|
"fastapi >= 0.115.12",
|
|
2179
|
-
"google-adk >= 1.16.0",
|
|
2180
|
-
"google-genai >= 1.0.0",
|
|
2181
|
-
|
|
2182
|
-
"opentelemetry-
|
|
2605
|
+
"google-adk >= 1.16.0, < 2.0.0",
|
|
2606
|
+
"google-genai >= 1.0.0, < 2.0.0",
|
|
2607
|
+
# 1.13.0 was yanked for broken imports.
|
|
2608
|
+
"opentelemetry-resourcedetector-gcp >= 1.9.0a0, < 2.0.0, != 1.13.0",
|
|
2609
|
+
"aws-opentelemetry-distro >= 0.17.0",
|
|
2183
2610
|
"uvicorn >= 0.34.3",
|
|
2184
2611
|
]
|
|
2185
2612
|
|
|
@@ -2287,6 +2714,62 @@ def add_numbers(a: int, b: int) -> int:
|
|
|
2287
2714
|
|
|
2288
2715
|
|
|
2289
2716
|
backend_tools = [add_numbers]
|
|
2717
|
+
|
|
2718
|
+
{{#if needsOs}}
|
|
2719
|
+
_MOUNT_PATHS = [
|
|
2720
|
+
{{#if sessionStorageMountPath}}"{{sessionStorageMountPath}}",{{/if}}
|
|
2721
|
+
{{#each efsMounts}}"{{mountPath}}",{{/each}}
|
|
2722
|
+
{{#each s3Mounts}}"{{mountPath}}",{{/each}}
|
|
2723
|
+
]
|
|
2724
|
+
|
|
2725
|
+
def _safe_resolve(path: str) -> str:
|
|
2726
|
+
resolved = os.path.realpath(path)
|
|
2727
|
+
if not any(resolved == os.path.realpath(m) or resolved.startswith(os.path.realpath(m) + os.sep) for m in _MOUNT_PATHS):
|
|
2728
|
+
raise ValueError(f"Path '{path}' is not within any configured mount ({', '.join(_MOUNT_PATHS)})")
|
|
2729
|
+
return resolved
|
|
2730
|
+
|
|
2731
|
+
@tool
|
|
2732
|
+
def file_read(path: str) -> str:
|
|
2733
|
+
"""Read a file from a mounted filesystem. Use the absolute path (e.g. /mnt/tools/data.txt)."""
|
|
2734
|
+
try:
|
|
2735
|
+
full_path = _safe_resolve(path)
|
|
2736
|
+
with open(full_path) as f:
|
|
2737
|
+
return f.read()
|
|
2738
|
+
except ValueError as e:
|
|
2739
|
+
return str(e)
|
|
2740
|
+
except OSError as e:
|
|
2741
|
+
return f"Error reading '{path}': {e.strerror}"
|
|
2742
|
+
|
|
2743
|
+
@tool
|
|
2744
|
+
def file_write(path: str, content: str) -> str:
|
|
2745
|
+
"""Write a file to a mounted filesystem. Use the absolute path (e.g. /mnt/tools/data.txt)."""
|
|
2746
|
+
try:
|
|
2747
|
+
full_path = _safe_resolve(path)
|
|
2748
|
+
parent = os.path.dirname(full_path)
|
|
2749
|
+
if parent:
|
|
2750
|
+
os.makedirs(parent, exist_ok=True)
|
|
2751
|
+
with open(full_path, "w") as f:
|
|
2752
|
+
f.write(content)
|
|
2753
|
+
return f"Written to {path}"
|
|
2754
|
+
except ValueError as e:
|
|
2755
|
+
return str(e)
|
|
2756
|
+
except OSError as e:
|
|
2757
|
+
return f"Error writing '{path}': {e.strerror}"
|
|
2758
|
+
|
|
2759
|
+
@tool
|
|
2760
|
+
def list_files(path: str) -> str:
|
|
2761
|
+
"""List files in a mounted filesystem directory. Use the absolute path (e.g. /mnt/tools)."""
|
|
2762
|
+
try:
|
|
2763
|
+
full_path = _safe_resolve(path)
|
|
2764
|
+
entries = os.listdir(full_path)
|
|
2765
|
+
return "\\n".join(entries) if entries else "(empty directory)"
|
|
2766
|
+
except ValueError as e:
|
|
2767
|
+
return str(e)
|
|
2768
|
+
except OSError as e:
|
|
2769
|
+
return f"Error listing '{path}': {e.strerror}"
|
|
2770
|
+
|
|
2771
|
+
backend_tools.extend([file_read, file_write, list_files])
|
|
2772
|
+
{{/if}}
|
|
2290
2773
|
model = load_model()
|
|
2291
2774
|
|
|
2292
2775
|
|
|
@@ -2486,10 +2969,11 @@ dependencies = [
|
|
|
2486
2969
|
{{#if (eq modelProvider "Anthropic")}}"langchain-anthropic >= 0.3.0",
|
|
2487
2970
|
{{/if}}{{#if (eq modelProvider "Bedrock")}}"langchain-aws >= 0.2.0",
|
|
2488
2971
|
{{/if}}{{#if (eq modelProvider "Gemini")}}"langchain-google-genai >= 2.0.0",
|
|
2972
|
+
"google-genai >= 1.0.0, < 2.0.0",
|
|
2489
2973
|
{{/if}}{{#if (eq modelProvider "OpenAI")}}"langchain-openai >= 0.2.0",
|
|
2490
|
-
{{/if}}"aws-opentelemetry-distro",
|
|
2974
|
+
{{/if}}"aws-opentelemetry-distro ~= 0.18.0",
|
|
2491
2975
|
"opentelemetry-instrumentation-langchain >= 0.59.0",
|
|
2492
|
-
"bedrock-agentcore >= 1.0
|
|
2976
|
+
"bedrock-agentcore >= 1.8.0",
|
|
2493
2977
|
"botocore[crt] >= 1.35.0",
|
|
2494
2978
|
"langgraph >= 0.3.25",
|
|
2495
2979
|
"langchain >= 0.3.0",
|
|
@@ -2598,9 +3082,71 @@ def add_numbers(a: int, b: int) -> int:
|
|
|
2598
3082
|
|
|
2599
3083
|
tools = [add_numbers]
|
|
2600
3084
|
|
|
3085
|
+
{{#if needsOs}}
|
|
3086
|
+
_MOUNT_PATHS = [
|
|
3087
|
+
{{#if sessionStorageMountPath}}"{{sessionStorageMountPath}}",{{/if}}
|
|
3088
|
+
{{#each efsMounts}}"{{mountPath}}",{{/each}}
|
|
3089
|
+
{{#each s3Mounts}}"{{mountPath}}",{{/each}}
|
|
3090
|
+
]
|
|
3091
|
+
|
|
3092
|
+
def _safe_resolve(path: str) -> str:
|
|
3093
|
+
resolved = os.path.realpath(path)
|
|
3094
|
+
if not any(resolved == os.path.realpath(m) or resolved.startswith(os.path.realpath(m) + os.sep) for m in _MOUNT_PATHS):
|
|
3095
|
+
raise ValueError(f"Path '{path}' is not within any configured mount ({', '.join(_MOUNT_PATHS)})")
|
|
3096
|
+
return resolved
|
|
3097
|
+
|
|
3098
|
+
@tool
|
|
3099
|
+
def file_read(path: str) -> str:
|
|
3100
|
+
"""Read a file from a mounted filesystem. Use the absolute path (e.g. /mnt/tools/data.txt)."""
|
|
3101
|
+
try:
|
|
3102
|
+
full_path = _safe_resolve(path)
|
|
3103
|
+
with open(full_path) as f:
|
|
3104
|
+
return f.read()
|
|
3105
|
+
except ValueError as e:
|
|
3106
|
+
return str(e)
|
|
3107
|
+
except OSError as e:
|
|
3108
|
+
return f"Error reading '{path}': {e.strerror}"
|
|
3109
|
+
|
|
3110
|
+
@tool
|
|
3111
|
+
def file_write(path: str, content: str) -> str:
|
|
3112
|
+
"""Write a file to a mounted filesystem. Use the absolute path (e.g. /mnt/tools/data.txt)."""
|
|
3113
|
+
try:
|
|
3114
|
+
full_path = _safe_resolve(path)
|
|
3115
|
+
parent = os.path.dirname(full_path)
|
|
3116
|
+
if parent:
|
|
3117
|
+
os.makedirs(parent, exist_ok=True)
|
|
3118
|
+
with open(full_path, "w") as f:
|
|
3119
|
+
f.write(content)
|
|
3120
|
+
return f"Written to {path}"
|
|
3121
|
+
except ValueError as e:
|
|
3122
|
+
return str(e)
|
|
3123
|
+
except OSError as e:
|
|
3124
|
+
return f"Error writing '{path}': {e.strerror}"
|
|
3125
|
+
|
|
3126
|
+
@tool
|
|
3127
|
+
def list_files(path: str) -> str:
|
|
3128
|
+
"""List files in a mounted filesystem directory. Use the absolute path (e.g. /mnt/tools)."""
|
|
3129
|
+
try:
|
|
3130
|
+
full_path = _safe_resolve(path)
|
|
3131
|
+
entries = os.listdir(full_path)
|
|
3132
|
+
return "\\n".join(entries) if entries else "(empty directory)"
|
|
3133
|
+
except ValueError as e:
|
|
3134
|
+
return str(e)
|
|
3135
|
+
except OSError as e:
|
|
3136
|
+
return f"Error listing '{path}': {e.strerror}"
|
|
3137
|
+
|
|
3138
|
+
tools.extend([file_read, file_write, list_files])
|
|
3139
|
+
{{/if}}
|
|
3140
|
+
|
|
2601
3141
|
agent = Agent(
|
|
2602
3142
|
model=load_model(),
|
|
2603
|
-
system_prompt="You are a helpful assistant. Use tools when appropriate.
|
|
3143
|
+
system_prompt="""You are a helpful assistant. Use tools when appropriate.
|
|
3144
|
+
{{#if needsOs}}
|
|
3145
|
+
You have access to the following mounted filesystems. Use file_read, file_write, and list_files with full absolute paths:
|
|
3146
|
+
{{#if sessionStorageMountPath}}- {{sessionStorageMountPath}}: ephemeral session storage (lost when session ends)
|
|
3147
|
+
{{/if}}{{#each efsMounts}}- {{mountPath}}: EFS persistent storage (persists across sessions and agent restarts)
|
|
3148
|
+
{{/each}}{{#each s3Mounts}}- {{mountPath}}: S3 Files persistent storage (durable, backed by S3)
|
|
3149
|
+
{{/each}}{{/if}}""",
|
|
2604
3150
|
tools=tools,
|
|
2605
3151
|
)
|
|
2606
3152
|
|
|
@@ -2768,8 +3314,8 @@ dependencies = [
|
|
|
2768
3314
|
{{#if (eq modelProvider "Anthropic")}}"anthropic >= 0.30.0",
|
|
2769
3315
|
{{/if}}"ag-ui-strands >= 0.1.7",
|
|
2770
3316
|
"ag-ui-protocol >= 0.1.10",
|
|
2771
|
-
"aws-opentelemetry-distro",
|
|
2772
|
-
"bedrock-agentcore >= 1.
|
|
3317
|
+
"aws-opentelemetry-distro ~= 0.18.0",
|
|
3318
|
+
"bedrock-agentcore >= 1.9.1",
|
|
2773
3319
|
"botocore[crt] >= 1.35.0",
|
|
2774
3320
|
"fastapi >= 0.115.12",
|
|
2775
3321
|
{{#if (eq modelProvider "Gemini")}}"google-genai >= 1.0.0",
|
|
@@ -2816,8 +3362,11 @@ def get_memory_session_manager(session_id: Optional[str], actor_id: str) -> Opti
|
|
|
2816
3362
|
{{#if (includes memoryProviders.[0].strategies "USER_PREFERENCE")}}
|
|
2817
3363
|
f"/users/{actor_id}/preferences": RetrievalConfig(top_k=3, relevance_score=0.5),
|
|
2818
3364
|
{{/if}}
|
|
3365
|
+
{{#if (includes memoryProviders.[0].strategies "EPISODIC")}}
|
|
3366
|
+
f"/episodes/{actor_id}/{session_id}": RetrievalConfig(top_k=5, relevance_score=0.5),
|
|
3367
|
+
{{/if}}
|
|
2819
3368
|
{{#if (includes memoryProviders.[0].strategies "SUMMARIZATION")}}
|
|
2820
|
-
f"/summaries/{actor_id}
|
|
3369
|
+
f"/summaries/{actor_id}": RetrievalConfig(top_k=3, relevance_score=0.5),
|
|
2821
3370
|
{{/if}}
|
|
2822
3371
|
}
|
|
2823
3372
|
{{/if}}
|
|
@@ -2852,6 +3401,11 @@ file defines a Starlette ASGI app with the AutoGen framework running within.
|
|
|
2852
3401
|
|
|
2853
3402
|
\`model/load.py\` instantiates your chosen model provider.
|
|
2854
3403
|
|
|
3404
|
+
## Input Validation
|
|
3405
|
+
|
|
3406
|
+
Validate invocation input before forwarding it to the agent framework. Keep plain prompts typed as strings and pass
|
|
3407
|
+
only prompt text to the agent.
|
|
3408
|
+
|
|
2855
3409
|
## Environment Variables
|
|
2856
3410
|
|
|
2857
3411
|
| Variable | Required | Description |
|
|
@@ -2863,7 +3417,8 @@ file defines a Starlette ASGI app with the AutoGen framework running within.
|
|
|
2863
3417
|
|
|
2864
3418
|
If installation was successful, a virtual environment is already created with dependencies installed.
|
|
2865
3419
|
|
|
2866
|
-
|
|
3420
|
+
Activate the environment with \`source .venv/bin/activate\` on macOS/Linux, \`.venv\\Scripts\\activate.bat\` in Windows
|
|
3421
|
+
Command Prompt, or \`.\\.venv\\Scripts\\activate.ps1\` in Windows PowerShell.
|
|
2867
3422
|
|
|
2868
3423
|
\`agentcore dev\` will start a local server on 0.0.0.0:8080.
|
|
2869
3424
|
|
|
@@ -2925,7 +3480,10 @@ Thumbs.db
|
|
|
2925
3480
|
`;
|
|
2926
3481
|
|
|
2927
3482
|
exports[`Assets Directory Snapshots > Python framework assets > python/python/http/autogen/base/main.py should match snapshot 1`] = `
|
|
2928
|
-
"
|
|
3483
|
+
"{{#if needsOs}}
|
|
3484
|
+
import os
|
|
3485
|
+
{{/if}}
|
|
3486
|
+
from collections import OrderedDict
|
|
2929
3487
|
from autogen_agentchat.agents import AssistantAgent
|
|
2930
3488
|
from autogen_core.tools import FunctionTool
|
|
2931
3489
|
from bedrock_agentcore.runtime import BedrockAgentCoreApp
|
|
@@ -2949,18 +3507,21 @@ add_numbers_tool = FunctionTool(
|
|
|
2949
3507
|
# Define a collection of tools used by the model
|
|
2950
3508
|
tools = [add_numbers_tool]
|
|
2951
3509
|
|
|
2952
|
-
{{#if
|
|
2953
|
-
|
|
3510
|
+
{{#if needsOs}}
|
|
3511
|
+
_MOUNT_PATHS = [
|
|
3512
|
+
{{#if sessionStorageMountPath}}"{{sessionStorageMountPath}}",{{/if}}
|
|
3513
|
+
{{#each efsMounts}}"{{mountPath}}",{{/each}}
|
|
3514
|
+
{{#each s3Mounts}}"{{mountPath}}",{{/each}}
|
|
3515
|
+
]
|
|
2954
3516
|
|
|
2955
3517
|
def _safe_resolve(path: str) -> str:
|
|
2956
|
-
|
|
2957
|
-
resolved
|
|
2958
|
-
|
|
2959
|
-
raise ValueError(f"Path '{path}' is outside the storage boundary")
|
|
3518
|
+
resolved = os.path.realpath(path)
|
|
3519
|
+
if not any(resolved == os.path.realpath(m) or resolved.startswith(os.path.realpath(m) + os.sep) for m in _MOUNT_PATHS):
|
|
3520
|
+
raise ValueError(f"Path '{path}' is not within any configured mount ({', '.join(_MOUNT_PATHS)})")
|
|
2960
3521
|
return resolved
|
|
2961
3522
|
|
|
2962
3523
|
def file_read(path: str) -> str:
|
|
2963
|
-
"""Read a file from
|
|
3524
|
+
"""Read a file from a mounted filesystem. Use the absolute path (e.g. /mnt/tools/data.txt)."""
|
|
2964
3525
|
try:
|
|
2965
3526
|
full_path = _safe_resolve(path)
|
|
2966
3527
|
with open(full_path) as f:
|
|
@@ -2971,7 +3532,7 @@ def file_read(path: str) -> str:
|
|
|
2971
3532
|
return f"Error reading '{path}': {e.strerror}"
|
|
2972
3533
|
|
|
2973
3534
|
def file_write(path: str, content: str) -> str:
|
|
2974
|
-
"""Write
|
|
3535
|
+
"""Write a file to a mounted filesystem. Use the absolute path (e.g. /mnt/tools/data.txt)."""
|
|
2975
3536
|
try:
|
|
2976
3537
|
full_path = _safe_resolve(path)
|
|
2977
3538
|
parent = os.path.dirname(full_path)
|
|
@@ -2985,48 +3546,75 @@ def file_write(path: str, content: str) -> str:
|
|
|
2985
3546
|
except OSError as e:
|
|
2986
3547
|
return f"Error writing '{path}': {e.strerror}"
|
|
2987
3548
|
|
|
2988
|
-
def list_files(
|
|
2989
|
-
"""List files in
|
|
3549
|
+
def list_files(path: str) -> str:
|
|
3550
|
+
"""List files in a mounted filesystem directory. Use the absolute path (e.g. /mnt/tools)."""
|
|
2990
3551
|
try:
|
|
2991
|
-
|
|
2992
|
-
entries = os.listdir(
|
|
3552
|
+
full_path = _safe_resolve(path)
|
|
3553
|
+
entries = os.listdir(full_path)
|
|
2993
3554
|
return "\\n".join(entries) if entries else "(empty directory)"
|
|
2994
3555
|
except ValueError as e:
|
|
2995
3556
|
return str(e)
|
|
2996
3557
|
except OSError as e:
|
|
2997
|
-
return f"Error listing '{
|
|
3558
|
+
return f"Error listing '{path}': {e.strerror}"
|
|
2998
3559
|
|
|
2999
3560
|
tools.extend([
|
|
3000
|
-
FunctionTool(file_read, description="Read a file from
|
|
3001
|
-
FunctionTool(file_write, description="Write
|
|
3002
|
-
FunctionTool(list_files, description="List files in
|
|
3561
|
+
FunctionTool(file_read, description="Read a file from a mounted filesystem. Use the absolute path (e.g. /mnt/tools/data.txt)."),
|
|
3562
|
+
FunctionTool(file_write, description="Write a file to a mounted filesystem. Use the absolute path (e.g. /mnt/tools/data.txt)."),
|
|
3563
|
+
FunctionTool(list_files, description="List files in a mounted filesystem directory. Use the absolute path (e.g. /mnt/tools)."),
|
|
3003
3564
|
])
|
|
3004
3565
|
{{/if}}
|
|
3005
3566
|
|
|
3006
3567
|
SYSTEM_MESSAGE = """
|
|
3007
3568
|
You are a helpful assistant. Use tools when appropriate.
|
|
3008
|
-
{{#if
|
|
3009
|
-
You have
|
|
3010
|
-
{{
|
|
3569
|
+
{{#if needsOs}}
|
|
3570
|
+
You have access to the following mounted filesystems. Use file_read, file_write, and list_files with full absolute paths:
|
|
3571
|
+
{{#if sessionStorageMountPath}}- {{sessionStorageMountPath}}: ephemeral session storage (lost when session ends)
|
|
3572
|
+
{{/if}}{{#each efsMounts}}- {{mountPath}}: EFS persistent storage (persists across sessions and agent restarts)
|
|
3573
|
+
{{/each}}{{#each s3Mounts}}- {{mountPath}}: S3 Files persistent storage (durable, backed by S3)
|
|
3574
|
+
{{/each}}{{/if}}
|
|
3011
3575
|
"""
|
|
3012
3576
|
|
|
3013
|
-
|
|
3014
|
-
|
|
3015
|
-
|
|
3577
|
+
# Reuses one AssistantAgent per session_id so each session keeps its own
|
|
3578
|
+
# in-process conversation history (best-effort; resets on cold start). Caches up
|
|
3579
|
+
# to 128 active sessions with LRU eviction (least-recently-used is dropped and
|
|
3580
|
+
# its history reset).
|
|
3581
|
+
_agents = OrderedDict()
|
|
3016
3582
|
|
|
3583
|
+
|
|
3584
|
+
async def get_or_create_agent(session_id):
|
|
3585
|
+
if session_id in _agents:
|
|
3586
|
+
_agents.move_to_end(session_id)
|
|
3587
|
+
return _agents[session_id]
|
|
3588
|
+
if len(_agents) >= 128:
|
|
3589
|
+
_agents.popitem(last=False)
|
|
3017
3590
|
# Get MCP Tools
|
|
3018
3591
|
mcp_tools = await get_streamable_http_mcp_tools()
|
|
3592
|
+
# Re-check after the await: a concurrent first-invocation for the same
|
|
3593
|
+
# session_id may have built and stored the agent while we were awaiting.
|
|
3594
|
+
# Don't overwrite it (that would orphan the agent the other request is using).
|
|
3595
|
+
if session_id not in _agents:
|
|
3596
|
+
_agents[session_id] = AssistantAgent(
|
|
3597
|
+
name="{{ name }}",
|
|
3598
|
+
model_client=load_model(),
|
|
3599
|
+
tools=tools + mcp_tools,
|
|
3600
|
+
system_message=SYSTEM_MESSAGE,
|
|
3601
|
+
)
|
|
3602
|
+
_agents.move_to_end(session_id)
|
|
3603
|
+
return _agents[session_id]
|
|
3019
3604
|
|
|
3020
|
-
|
|
3021
|
-
|
|
3022
|
-
|
|
3023
|
-
|
|
3024
|
-
tools=tools + mcp_tools,
|
|
3025
|
-
system_message=SYSTEM_MESSAGE,
|
|
3026
|
-
)
|
|
3605
|
+
|
|
3606
|
+
@app.entrypoint
|
|
3607
|
+
async def invoke(payload, context):
|
|
3608
|
+
log.info("Invoking Agent.....")
|
|
3027
3609
|
|
|
3028
3610
|
# Process the user prompt
|
|
3029
3611
|
prompt = payload.get("prompt", "What can you help me with?")
|
|
3612
|
+
if not isinstance(prompt, str):
|
|
3613
|
+
raise ValueError("prompt must be a string")
|
|
3614
|
+
session_id = getattr(context, "session_id", "default-session")
|
|
3615
|
+
|
|
3616
|
+
# Reuse the per-session agent (preserves conversation history)
|
|
3617
|
+
agent = await get_or_create_agent(session_id)
|
|
3030
3618
|
|
|
3031
3619
|
# Run the agent
|
|
3032
3620
|
result = await agent.run(task=prompt)
|
|
@@ -3238,7 +3826,7 @@ dependencies = [
|
|
|
3238
3826
|
"autogen-ext[mcp] >= 0.7.5",
|
|
3239
3827
|
"opentelemetry-distro",
|
|
3240
3828
|
"opentelemetry-exporter-otlp",
|
|
3241
|
-
"bedrock-agentcore >= 1.0
|
|
3829
|
+
"bedrock-agentcore >= 1.8.0",
|
|
3242
3830
|
"botocore[crt] >= 1.35.0",
|
|
3243
3831
|
"tiktoken",
|
|
3244
3832
|
{{#if (eq modelProvider "Bedrock")}}
|
|
@@ -3276,6 +3864,11 @@ file defines a Starlette ASGI app with the Google ADK framework running within.
|
|
|
3276
3864
|
|
|
3277
3865
|
\`model/load.py\` instantiates your chosen model provider (Gemini).
|
|
3278
3866
|
|
|
3867
|
+
## Input Validation
|
|
3868
|
+
|
|
3869
|
+
Validate invocation input before forwarding it to the agent framework. Keep plain prompts typed as strings and pass
|
|
3870
|
+
only prompt text to the agent.
|
|
3871
|
+
|
|
3279
3872
|
## Environment Variables
|
|
3280
3873
|
|
|
3281
3874
|
| Variable | Required | Description |
|
|
@@ -3287,7 +3880,8 @@ file defines a Starlette ASGI app with the Google ADK framework running within.
|
|
|
3287
3880
|
|
|
3288
3881
|
If installation was successful, a virtual environment is already created with dependencies installed.
|
|
3289
3882
|
|
|
3290
|
-
|
|
3883
|
+
Activate the environment with \`source .venv/bin/activate\` on macOS/Linux, \`.venv\\Scripts\\activate.bat\` in Windows
|
|
3884
|
+
Command Prompt, or \`.\\.venv\\Scripts\\activate.ps1\` in Windows PowerShell.
|
|
3291
3885
|
|
|
3292
3886
|
\`agentcore dev\` will start a local server on 0.0.0.0:8080.
|
|
3293
3887
|
|
|
@@ -3349,7 +3943,10 @@ Thumbs.db
|
|
|
3349
3943
|
`;
|
|
3350
3944
|
|
|
3351
3945
|
exports[`Assets Directory Snapshots > Python framework assets > python/python/http/googleadk/base/main.py should match snapshot 1`] = `
|
|
3352
|
-
"
|
|
3946
|
+
"{{#if needsOs}}
|
|
3947
|
+
import os
|
|
3948
|
+
{{/if}}
|
|
3949
|
+
from collections import OrderedDict
|
|
3353
3950
|
from google.adk.agents import Agent
|
|
3354
3951
|
from google.adk.runners import Runner
|
|
3355
3952
|
from google.adk.sessions import InMemorySessionService
|
|
@@ -3380,18 +3977,21 @@ def add_numbers(a: int, b: int) -> int:
|
|
|
3380
3977
|
# Define a collection of tools used by the model
|
|
3381
3978
|
tools = [add_numbers]
|
|
3382
3979
|
|
|
3383
|
-
{{#if
|
|
3384
|
-
|
|
3980
|
+
{{#if needsOs}}
|
|
3981
|
+
_MOUNT_PATHS = [
|
|
3982
|
+
{{#if sessionStorageMountPath}}"{{sessionStorageMountPath}}",{{/if}}
|
|
3983
|
+
{{#each efsMounts}}"{{mountPath}}",{{/each}}
|
|
3984
|
+
{{#each s3Mounts}}"{{mountPath}}",{{/each}}
|
|
3985
|
+
]
|
|
3385
3986
|
|
|
3386
3987
|
def _safe_resolve(path: str) -> str:
|
|
3387
|
-
|
|
3388
|
-
resolved
|
|
3389
|
-
|
|
3390
|
-
raise ValueError(f"Path '{path}' is outside the storage boundary")
|
|
3988
|
+
resolved = os.path.realpath(path)
|
|
3989
|
+
if not any(resolved == os.path.realpath(m) or resolved.startswith(os.path.realpath(m) + os.sep) for m in _MOUNT_PATHS):
|
|
3990
|
+
raise ValueError(f"Path '{path}' is not within any configured mount ({', '.join(_MOUNT_PATHS)})")
|
|
3391
3991
|
return resolved
|
|
3392
3992
|
|
|
3393
3993
|
def file_read(path: str) -> str:
|
|
3394
|
-
"""Read a file from
|
|
3994
|
+
"""Read a file from a mounted filesystem. Use the absolute path (e.g. /mnt/tools/data.txt)."""
|
|
3395
3995
|
try:
|
|
3396
3996
|
full_path = _safe_resolve(path)
|
|
3397
3997
|
with open(full_path) as f:
|
|
@@ -3402,7 +4002,7 @@ def file_read(path: str) -> str:
|
|
|
3402
4002
|
return f"Error reading '{path}': {e.strerror}"
|
|
3403
4003
|
|
|
3404
4004
|
def file_write(path: str, content: str) -> str:
|
|
3405
|
-
"""Write
|
|
4005
|
+
"""Write a file to a mounted filesystem. Use the absolute path (e.g. /mnt/tools/data.txt)."""
|
|
3406
4006
|
try:
|
|
3407
4007
|
full_path = _safe_resolve(path)
|
|
3408
4008
|
parent = os.path.dirname(full_path)
|
|
@@ -3416,25 +4016,28 @@ def file_write(path: str, content: str) -> str:
|
|
|
3416
4016
|
except OSError as e:
|
|
3417
4017
|
return f"Error writing '{path}': {e.strerror}"
|
|
3418
4018
|
|
|
3419
|
-
def list_files(
|
|
3420
|
-
"""List files in
|
|
4019
|
+
def list_files(path: str) -> str:
|
|
4020
|
+
"""List files in a mounted filesystem directory. Use the absolute path (e.g. /mnt/tools)."""
|
|
3421
4021
|
try:
|
|
3422
|
-
|
|
3423
|
-
entries = os.listdir(
|
|
4022
|
+
full_path = _safe_resolve(path)
|
|
4023
|
+
entries = os.listdir(full_path)
|
|
3424
4024
|
return "\\n".join(entries) if entries else "(empty directory)"
|
|
3425
4025
|
except ValueError as e:
|
|
3426
4026
|
return str(e)
|
|
3427
4027
|
except OSError as e:
|
|
3428
|
-
return f"Error listing '{
|
|
4028
|
+
return f"Error listing '{path}': {e.strerror}"
|
|
3429
4029
|
|
|
3430
4030
|
tools.extend([file_read, file_write, list_files])
|
|
3431
4031
|
{{/if}}
|
|
3432
4032
|
|
|
3433
4033
|
AGENT_INSTRUCTION = """
|
|
3434
4034
|
I can answer your questions using the knowledge I have!
|
|
3435
|
-
{{#if
|
|
3436
|
-
You have
|
|
3437
|
-
{{
|
|
4035
|
+
{{#if needsOs}}
|
|
4036
|
+
You have access to the following mounted filesystems. Use file_read, file_write, and list_files with full absolute paths:
|
|
4037
|
+
{{#if sessionStorageMountPath}}- {{sessionStorageMountPath}}: ephemeral session storage (lost when session ends)
|
|
4038
|
+
{{/if}}{{#each efsMounts}}- {{mountPath}}: EFS persistent storage (persists across sessions and agent restarts)
|
|
4039
|
+
{{/each}}{{#each s3Mounts}}- {{mountPath}}: S3 Files persistent storage (durable, backed by S3)
|
|
4040
|
+
{{/each}}{{/if}}
|
|
3438
4041
|
"""
|
|
3439
4042
|
|
|
3440
4043
|
# Get MCP Toolset
|
|
@@ -3464,21 +4067,57 @@ agent = Agent(
|
|
|
3464
4067
|
)
|
|
3465
4068
|
|
|
3466
4069
|
|
|
3467
|
-
#
|
|
3468
|
-
|
|
3469
|
-
|
|
3470
|
-
|
|
3471
|
-
|
|
4070
|
+
# Module-level session service and runner preserve history across invocations.
|
|
4071
|
+
# InMemorySessionService retains every (app_name, user_id, session_id) triple
|
|
4072
|
+
# forever, so we bound it to 128 active sessions with LRU eviction (the
|
|
4073
|
+
# least-recently-used session is deleted and its history reset) to keep a
|
|
4074
|
+
# long-running process from growing without limit. For durable history, swap in
|
|
4075
|
+
# a persistent session service (e.g. DatabaseSessionService).
|
|
4076
|
+
_SESSION_LIMIT = 128
|
|
4077
|
+
_session_service = InMemorySessionService()
|
|
4078
|
+
_session_keys = OrderedDict()
|
|
4079
|
+
_runner = None
|
|
4080
|
+
|
|
4081
|
+
|
|
4082
|
+
def get_or_create_runner():
|
|
4083
|
+
global _runner
|
|
4084
|
+
if _runner is None:
|
|
4085
|
+
ensure_credentials_loaded()
|
|
4086
|
+
_runner = Runner(
|
|
4087
|
+
agent=agent,
|
|
4088
|
+
app_name=APP_NAME,
|
|
4089
|
+
session_service=_session_service,
|
|
4090
|
+
)
|
|
4091
|
+
return _runner
|
|
4092
|
+
|
|
4093
|
+
|
|
4094
|
+
async def get_or_create_session(user_id, session_id):
|
|
4095
|
+
key = (user_id, session_id)
|
|
4096
|
+
if key in _session_keys:
|
|
4097
|
+
_session_keys.move_to_end(key)
|
|
4098
|
+
else:
|
|
4099
|
+
while len(_session_keys) >= _SESSION_LIMIT:
|
|
4100
|
+
(old_user_id, old_session_id), _ = _session_keys.popitem(last=False)
|
|
4101
|
+
await _session_service.delete_session(
|
|
4102
|
+
app_name=APP_NAME, user_id=old_user_id, session_id=old_session_id
|
|
4103
|
+
)
|
|
4104
|
+
_session_keys[key] = True
|
|
4105
|
+
|
|
4106
|
+
session = await _session_service.get_session(
|
|
3472
4107
|
app_name=APP_NAME, user_id=user_id, session_id=session_id
|
|
3473
4108
|
)
|
|
3474
|
-
|
|
3475
|
-
|
|
4109
|
+
if session is None:
|
|
4110
|
+
session = await _session_service.create_session(
|
|
4111
|
+
app_name=APP_NAME, user_id=user_id, session_id=session_id
|
|
4112
|
+
)
|
|
4113
|
+
return session
|
|
3476
4114
|
|
|
3477
4115
|
|
|
3478
4116
|
# Agent Interaction
|
|
3479
4117
|
async def call_agent_async(query, user_id, session_id):
|
|
3480
4118
|
content = types.Content(role="user", parts=[types.Part(text=query)])
|
|
3481
|
-
|
|
4119
|
+
runner = get_or_create_runner()
|
|
4120
|
+
session = await get_or_create_session(user_id, session_id)
|
|
3482
4121
|
events = runner.run_async(
|
|
3483
4122
|
user_id=user_id, session_id=session.id, new_message=content
|
|
3484
4123
|
)
|
|
@@ -3497,6 +4136,8 @@ async def invoke(payload, context):
|
|
|
3497
4136
|
|
|
3498
4137
|
# Process the user prompt
|
|
3499
4138
|
prompt = payload.get("prompt", "What can you help me with?")
|
|
4139
|
+
if not isinstance(prompt, str):
|
|
4140
|
+
raise ValueError("prompt must be a string")
|
|
3500
4141
|
session_id = getattr(context, "session_id", "default_session")
|
|
3501
4142
|
user_id = payload.get("user_id", "default_user")
|
|
3502
4143
|
|
|
@@ -3554,20 +4195,20 @@ def get_all_gateway_mcp_toolsets() -> list[MCPToolset]:
|
|
|
3554
4195
|
{{#each gatewayProviders}}
|
|
3555
4196
|
url = os.environ.get("{{envVarName}}")
|
|
3556
4197
|
if url:
|
|
3557
|
-
|
|
4198
|
+
{{#if (eq authType "AWS_IAM")}}
|
|
3558
4199
|
session = create_aws_session()
|
|
3559
4200
|
auth = SigV4HTTPXAuth(session.get_credentials(), "bedrock-agentcore", session.region_name)
|
|
3560
|
-
toolsets.append(MCPToolset(connection_params=StreamableHTTPConnectionParams(
|
|
4201
|
+
toolsets.append(MCPToolset(tool_name_prefix="{{snakeCase name}}", connection_params=StreamableHTTPConnectionParams(
|
|
3561
4202
|
url=url,
|
|
3562
4203
|
httpx_client_factory=lambda **kwargs: httpx.AsyncClient(auth=auth, **kwargs)
|
|
3563
4204
|
)))
|
|
3564
|
-
|
|
4205
|
+
{{else if (eq authType "CUSTOM_JWT")}}
|
|
3565
4206
|
token = _get_bearer_token_{{snakeCase name}}()
|
|
3566
4207
|
headers = {"Authorization": f"Bearer {token}"} if token else None
|
|
3567
|
-
toolsets.append(MCPToolset(connection_params=StreamableHTTPConnectionParams(url=url, headers=headers)))
|
|
3568
|
-
|
|
3569
|
-
toolsets.append(MCPToolset(connection_params=StreamableHTTPConnectionParams(url=url)))
|
|
3570
|
-
|
|
4208
|
+
toolsets.append(MCPToolset(tool_name_prefix="{{snakeCase name}}", connection_params=StreamableHTTPConnectionParams(url=url, headers=headers)))
|
|
4209
|
+
{{else}}
|
|
4210
|
+
toolsets.append(MCPToolset(tool_name_prefix="{{snakeCase name}}", connection_params=StreamableHTTPConnectionParams(url=url)))
|
|
4211
|
+
{{/if}}
|
|
3571
4212
|
else:
|
|
3572
4213
|
logger.warning("{{envVarName}} not set — {{name}} gateway tools unavailable")
|
|
3573
4214
|
{{/each}}
|
|
@@ -3658,9 +4299,11 @@ description = "AgentCore Runtime Application using Google ADK"
|
|
|
3658
4299
|
readme = "README.md"
|
|
3659
4300
|
requires-python = ">=3.10"
|
|
3660
4301
|
dependencies = [
|
|
3661
|
-
"opentelemetry-distro",
|
|
3662
|
-
"
|
|
3663
|
-
"google-
|
|
4302
|
+
"aws-opentelemetry-distro >= 0.17.0",
|
|
4303
|
+
"google-adk >= 1.35.0, < 2.0.0",
|
|
4304
|
+
"google-genai >= 1.0.0, < 2.0.0",
|
|
4305
|
+
# 1.13.0 was yanked for broken imports.
|
|
4306
|
+
"opentelemetry-resourcedetector-gcp >= 1.9.0a0, < 2.0.0, != 1.13.0",
|
|
3664
4307
|
"bedrock-agentcore >= 1.0.3",
|
|
3665
4308
|
"botocore[crt] >= 1.35.0",
|
|
3666
4309
|
{{#if hasGateway}}{{#if (includes gatewayAuthTypes "AWS_IAM")}}"mcp-proxy-for-aws >= 1.1.0",
|
|
@@ -3688,6 +4331,11 @@ file defines a Starlette ASGI app with the LangChain/LangGraph framework running
|
|
|
3688
4331
|
|
|
3689
4332
|
\`model/load.py\` instantiates your chosen model provider.
|
|
3690
4333
|
|
|
4334
|
+
## Input Validation
|
|
4335
|
+
|
|
4336
|
+
Validate invocation input before forwarding it to the agent framework. Keep plain prompts typed as strings and pass
|
|
4337
|
+
only prompt text to the agent.
|
|
4338
|
+
|
|
3691
4339
|
## Environment Variables
|
|
3692
4340
|
|
|
3693
4341
|
| Variable | Required | Description |
|
|
@@ -3699,7 +4347,8 @@ file defines a Starlette ASGI app with the LangChain/LangGraph framework running
|
|
|
3699
4347
|
|
|
3700
4348
|
If installation was successful, a virtual environment is already created with dependencies installed.
|
|
3701
4349
|
|
|
3702
|
-
|
|
4350
|
+
Activate the environment with \`source .venv/bin/activate\` on macOS/Linux, \`.venv\\Scripts\\activate.bat\` in Windows
|
|
4351
|
+
Command Prompt, or \`.\\.venv\\Scripts\\activate.ps1\` in Windows PowerShell.
|
|
3703
4352
|
|
|
3704
4353
|
\`agentcore dev\` will start a local server on 0.0.0.0:8080.
|
|
3705
4354
|
|
|
@@ -3761,10 +4410,20 @@ Thumbs.db
|
|
|
3761
4410
|
`;
|
|
3762
4411
|
|
|
3763
4412
|
exports[`Assets Directory Snapshots > Python framework assets > python/python/http/langchain_langgraph/base/main.py should match snapshot 1`] = `
|
|
3764
|
-
"
|
|
3765
|
-
|
|
4413
|
+
"{{#if needsOs}}
|
|
4414
|
+
import os
|
|
4415
|
+
{{/if}}
|
|
4416
|
+
from collections import OrderedDict
|
|
4417
|
+
from typing import Any
|
|
4418
|
+
|
|
4419
|
+
from langchain_core.messages import HumanMessage{{#if hasConfigBundle}}, SystemMessage{{/if}}
|
|
4420
|
+
from langgraph.checkpoint.memory import InMemorySaver
|
|
3766
4421
|
from langgraph.prebuilt import create_react_agent
|
|
3767
4422
|
from langchain.tools import tool
|
|
4423
|
+
{{#if hasConfigBundle}}
|
|
4424
|
+
from langchain_core.callbacks import BaseCallbackHandler
|
|
4425
|
+
from bedrock_agentcore.runtime.context import BedrockAgentCoreContext
|
|
4426
|
+
{{/if}}
|
|
3768
4427
|
from opentelemetry.instrumentation.langchain import LangchainInstrumentor
|
|
3769
4428
|
from bedrock_agentcore.runtime import BedrockAgentCoreApp
|
|
3770
4429
|
from model.load import load_model
|
|
@@ -3788,6 +4447,17 @@ def get_or_create_model():
|
|
|
3788
4447
|
return _llm
|
|
3789
4448
|
|
|
3790
4449
|
|
|
4450
|
+
DEFAULT_SYSTEM_PROMPT = """
|
|
4451
|
+
You are a helpful assistant. Use tools when appropriate.
|
|
4452
|
+
{{#if needsOs}}
|
|
4453
|
+
You have access to the following mounted filesystems. Use file_read, file_write, and list_files with full absolute paths:
|
|
4454
|
+
{{#if sessionStorageMountPath}}- {{sessionStorageMountPath}}: ephemeral session storage (lost when session ends)
|
|
4455
|
+
{{/if}}{{#each efsMounts}}- {{mountPath}}: EFS persistent storage (persists across sessions and agent restarts)
|
|
4456
|
+
{{/each}}{{#each s3Mounts}}- {{mountPath}}: S3 Files persistent storage (durable, backed by S3)
|
|
4457
|
+
{{/each}}{{/if}}
|
|
4458
|
+
"""
|
|
4459
|
+
|
|
4460
|
+
|
|
3791
4461
|
# Define a simple function tool
|
|
3792
4462
|
@tool
|
|
3793
4463
|
def add_numbers(a: int, b: int) -> int:
|
|
@@ -3798,19 +4468,42 @@ def add_numbers(a: int, b: int) -> int:
|
|
|
3798
4468
|
# Define a collection of tools used by the model
|
|
3799
4469
|
tools = [add_numbers]
|
|
3800
4470
|
|
|
3801
|
-
|
|
3802
|
-
|
|
4471
|
+
# Module-level checkpointer preserves conversation history across invocations.
|
|
4472
|
+
# InMemorySaver keeps every thread_id (= session_id) checkpoint in memory
|
|
4473
|
+
# forever, so we bound it to 128 active threads with LRU eviction (the
|
|
4474
|
+
# least-recently-used thread is deleted and its history reset) to keep a
|
|
4475
|
+
# long-running process from growing without limit. For durable history, swap in
|
|
4476
|
+
# a persistent checkpointer (e.g. SqliteSaver/AsyncSqliteSaver with a file path).
|
|
4477
|
+
_CHECKPOINT_LIMIT = 128
|
|
4478
|
+
_checkpointer = InMemorySaver()
|
|
4479
|
+
_thread_ids = OrderedDict()
|
|
4480
|
+
|
|
4481
|
+
|
|
4482
|
+
def touch_thread(thread_id):
|
|
4483
|
+
if thread_id in _thread_ids:
|
|
4484
|
+
_thread_ids.move_to_end(thread_id)
|
|
4485
|
+
return
|
|
4486
|
+
while len(_thread_ids) >= _CHECKPOINT_LIMIT:
|
|
4487
|
+
evicted, _ = _thread_ids.popitem(last=False)
|
|
4488
|
+
_checkpointer.delete_thread(evicted)
|
|
4489
|
+
_thread_ids[thread_id] = True
|
|
4490
|
+
|
|
4491
|
+
{{#if needsOs}}
|
|
4492
|
+
_MOUNT_PATHS = [
|
|
4493
|
+
{{#if sessionStorageMountPath}}"{{sessionStorageMountPath}}",{{/if}}
|
|
4494
|
+
{{#each efsMounts}}"{{mountPath}}",{{/each}}
|
|
4495
|
+
{{#each s3Mounts}}"{{mountPath}}",{{/each}}
|
|
4496
|
+
]
|
|
3803
4497
|
|
|
3804
4498
|
def _safe_resolve(path: str) -> str:
|
|
3805
|
-
|
|
3806
|
-
resolved
|
|
3807
|
-
|
|
3808
|
-
raise ValueError(f"Path '{path}' is outside the storage boundary")
|
|
4499
|
+
resolved = os.path.realpath(path)
|
|
4500
|
+
if not any(resolved == os.path.realpath(m) or resolved.startswith(os.path.realpath(m) + os.sep) for m in _MOUNT_PATHS):
|
|
4501
|
+
raise ValueError(f"Path '{path}' is not within any configured mount ({', '.join(_MOUNT_PATHS)})")
|
|
3809
4502
|
return resolved
|
|
3810
4503
|
|
|
3811
4504
|
@tool
|
|
3812
4505
|
def file_read(path: str) -> str:
|
|
3813
|
-
"""Read a file from
|
|
4506
|
+
"""Read a file from a mounted filesystem. Use the absolute path (e.g. /mnt/tools/data.txt)."""
|
|
3814
4507
|
try:
|
|
3815
4508
|
full_path = _safe_resolve(path)
|
|
3816
4509
|
with open(full_path) as f:
|
|
@@ -3822,7 +4515,7 @@ def file_read(path: str) -> str:
|
|
|
3822
4515
|
|
|
3823
4516
|
@tool
|
|
3824
4517
|
def file_write(path: str, content: str) -> str:
|
|
3825
|
-
"""Write
|
|
4518
|
+
"""Write a file to a mounted filesystem. Use the absolute path (e.g. /mnt/tools/data.txt)."""
|
|
3826
4519
|
try:
|
|
3827
4520
|
full_path = _safe_resolve(path)
|
|
3828
4521
|
parent = os.path.dirname(full_path)
|
|
@@ -3837,27 +4530,42 @@ def file_write(path: str, content: str) -> str:
|
|
|
3837
4530
|
return f"Error writing '{path}': {e.strerror}"
|
|
3838
4531
|
|
|
3839
4532
|
@tool
|
|
3840
|
-
def list_files(
|
|
3841
|
-
"""List files in
|
|
4533
|
+
def list_files(path: str) -> str:
|
|
4534
|
+
"""List files in a mounted filesystem directory. Use the absolute path (e.g. /mnt/tools)."""
|
|
3842
4535
|
try:
|
|
3843
|
-
|
|
3844
|
-
entries = os.listdir(
|
|
4536
|
+
full_path = _safe_resolve(path)
|
|
4537
|
+
entries = os.listdir(full_path)
|
|
3845
4538
|
return "\\n".join(entries) if entries else "(empty directory)"
|
|
3846
4539
|
except ValueError as e:
|
|
3847
4540
|
return str(e)
|
|
3848
4541
|
except OSError as e:
|
|
3849
|
-
return f"Error listing '{
|
|
4542
|
+
return f"Error listing '{path}': {e.strerror}"
|
|
3850
4543
|
|
|
3851
4544
|
tools.extend([file_read, file_write, list_files])
|
|
3852
4545
|
{{/if}}
|
|
3853
4546
|
|
|
3854
|
-
|
|
3855
|
-
|
|
3856
|
-
|
|
3857
|
-
|
|
3858
|
-
|
|
3859
|
-
|
|
4547
|
+
{{#if hasConfigBundle}}
|
|
4548
|
+
|
|
4549
|
+
class ConfigBundleCallback(BaseCallbackHandler):
|
|
4550
|
+
"""Injects config bundle values into LangGraph agent at runtime.
|
|
4551
|
+
|
|
4552
|
+
BedrockAgentCoreContext.get_config_bundle() fetches the component configuration
|
|
4553
|
+
for the current runtime ARN from the config bundle service. The SDK caches the
|
|
4554
|
+
result and refreshes on bundle version changes.
|
|
4555
|
+
"""
|
|
3860
4556
|
|
|
4557
|
+
def on_chain_start(self, serialized: dict, inputs: dict, **kwargs: Any) -> None:
|
|
4558
|
+
config = BedrockAgentCoreContext.get_config_bundle()
|
|
4559
|
+
prompt = config.get("systemPrompt", DEFAULT_SYSTEM_PROMPT)
|
|
4560
|
+
|
|
4561
|
+
messages = inputs.get("messages", [])
|
|
4562
|
+
if messages and isinstance(messages[0], SystemMessage):
|
|
4563
|
+
messages[0] = SystemMessage(content=prompt)
|
|
4564
|
+
else:
|
|
4565
|
+
messages.insert(0, SystemMessage(content=prompt))
|
|
4566
|
+
inputs["messages"] = messages
|
|
4567
|
+
|
|
4568
|
+
{{/if}}
|
|
3861
4569
|
|
|
3862
4570
|
@app.entrypoint
|
|
3863
4571
|
async def invoke(payload, context):
|
|
@@ -3875,15 +4583,51 @@ async def invoke(payload, context):
|
|
|
3875
4583
|
if mcp_client:
|
|
3876
4584
|
mcp_tools = await mcp_client.get_tools()
|
|
3877
4585
|
|
|
3878
|
-
# Define the agent using create_react_agent
|
|
3879
|
-
|
|
4586
|
+
# Define the agent using create_react_agent (checkpointer is shared across invocations)
|
|
4587
|
+
{{#if hasConfigBundle}}
|
|
4588
|
+
graph = create_react_agent(
|
|
4589
|
+
get_or_create_model(),
|
|
4590
|
+
tools=mcp_tools + tools,
|
|
4591
|
+
prompt=DEFAULT_SYSTEM_PROMPT,
|
|
4592
|
+
checkpointer=_checkpointer,
|
|
4593
|
+
)
|
|
4594
|
+
callback = ConfigBundleCallback()
|
|
3880
4595
|
|
|
3881
4596
|
# Process the user prompt
|
|
3882
4597
|
prompt = payload.get("prompt", "What can you help me with?")
|
|
4598
|
+
if not isinstance(prompt, str):
|
|
4599
|
+
raise ValueError("prompt must be a string")
|
|
4600
|
+
session_id = getattr(context, "session_id", "default-session")
|
|
4601
|
+
touch_thread(session_id)
|
|
3883
4602
|
log.info(f"Agent input: {prompt}")
|
|
3884
4603
|
|
|
3885
|
-
# Run the agent
|
|
3886
|
-
result = await graph.ainvoke(
|
|
4604
|
+
# Run the agent with config bundle callback (checkpointer auto-loads/saves history per session)
|
|
4605
|
+
result = await graph.ainvoke(
|
|
4606
|
+
{"messages": [HumanMessage(content=prompt)]},
|
|
4607
|
+
config={"callbacks": [callback], "configurable": {"thread_id": session_id}},
|
|
4608
|
+
)
|
|
4609
|
+
{{else}}
|
|
4610
|
+
graph = create_react_agent(
|
|
4611
|
+
get_or_create_model(),
|
|
4612
|
+
tools=mcp_tools + tools,
|
|
4613
|
+
prompt=DEFAULT_SYSTEM_PROMPT,
|
|
4614
|
+
checkpointer=_checkpointer,
|
|
4615
|
+
)
|
|
4616
|
+
|
|
4617
|
+
# Process the user prompt
|
|
4618
|
+
prompt = payload.get("prompt", "What can you help me with?")
|
|
4619
|
+
if not isinstance(prompt, str):
|
|
4620
|
+
raise ValueError("prompt must be a string")
|
|
4621
|
+
session_id = getattr(context, "session_id", "default-session")
|
|
4622
|
+
touch_thread(session_id)
|
|
4623
|
+
log.info(f"Agent input: {prompt}")
|
|
4624
|
+
|
|
4625
|
+
# Run the agent (checkpointer auto-loads/saves history per session)
|
|
4626
|
+
result = await graph.ainvoke(
|
|
4627
|
+
{"messages": [HumanMessage(content=prompt)]},
|
|
4628
|
+
config={"configurable": {"thread_id": session_id}},
|
|
4629
|
+
)
|
|
4630
|
+
{{/if}}
|
|
3887
4631
|
|
|
3888
4632
|
# Return result
|
|
3889
4633
|
output = result["messages"][-1].content
|
|
@@ -3936,23 +4680,25 @@ def get_all_gateway_mcp_client() -> MultiServerMCPClient | None:
|
|
|
3936
4680
|
{{#each gatewayProviders}}
|
|
3937
4681
|
url = os.environ.get("{{envVarName}}")
|
|
3938
4682
|
if url:
|
|
3939
|
-
|
|
4683
|
+
{{#if (eq authType "AWS_IAM")}}
|
|
3940
4684
|
session = create_aws_session()
|
|
3941
4685
|
auth = SigV4HTTPXAuth(session.get_credentials(), "bedrock-agentcore", session.region_name)
|
|
3942
|
-
servers["{{name}}"] = {"transport": "streamable_http", "url": url, "auth": auth}
|
|
3943
|
-
|
|
4686
|
+
servers["{{snakeCase name}}"] = {"transport": "streamable_http", "url": url, "auth": auth}
|
|
4687
|
+
{{else if (eq authType "CUSTOM_JWT")}}
|
|
3944
4688
|
token = _get_bearer_token_{{snakeCase name}}()
|
|
3945
4689
|
headers = {"Authorization": f"Bearer {token}"} if token else None
|
|
3946
|
-
servers["{{name}}"] = {"transport": "streamable_http", "url": url, "headers": headers}
|
|
3947
|
-
|
|
3948
|
-
servers["{{name}}"] = {"transport": "streamable_http", "url": url}
|
|
3949
|
-
|
|
4690
|
+
servers["{{snakeCase name}}"] = {"transport": "streamable_http", "url": url, "headers": headers}
|
|
4691
|
+
{{else}}
|
|
4692
|
+
servers["{{snakeCase name}}"] = {"transport": "streamable_http", "url": url}
|
|
4693
|
+
{{/if}}
|
|
3950
4694
|
else:
|
|
3951
4695
|
logger.warning("{{envVarName}} not set — {{name}} gateway tools unavailable")
|
|
3952
4696
|
{{/each}}
|
|
3953
4697
|
if not servers:
|
|
3954
4698
|
return None
|
|
3955
|
-
|
|
4699
|
+
# tool_name_prefix namespaces each gateway's tools by server key so multiple
|
|
4700
|
+
# gateways exposing the same tool (e.g. x_amz_bedrock_agentcore_search) don't collide.
|
|
4701
|
+
return MultiServerMCPClient(servers, tool_name_prefix=True)
|
|
3956
4702
|
{{else}}
|
|
3957
4703
|
{{#if isVpc}}
|
|
3958
4704
|
# VPC mode: external MCP endpoints are not reachable without a NAT gateway.
|
|
@@ -4126,13 +4872,13 @@ description = "AgentCore Runtime Application using LangChain/LangGraph"
|
|
|
4126
4872
|
readme = "README.md"
|
|
4127
4873
|
requires-python = ">=3.10"
|
|
4128
4874
|
dependencies = [
|
|
4129
|
-
"aws-opentelemetry-distro",
|
|
4875
|
+
"aws-opentelemetry-distro ~= 0.18.0",
|
|
4130
4876
|
"opentelemetry-instrumentation-langchain >= 0.59.0",
|
|
4131
4877
|
"langgraph >= 1.0.2",
|
|
4132
|
-
"mcp
|
|
4133
|
-
"langchain-mcp-adapters >= 0.1
|
|
4878
|
+
"mcp ~= 1.24.0",
|
|
4879
|
+
"langchain-mcp-adapters >= 0.3.1",
|
|
4134
4880
|
"langchain >= 1.0.3",
|
|
4135
|
-
"bedrock-agentcore >= 1.0
|
|
4881
|
+
"bedrock-agentcore >= 1.8.0",
|
|
4136
4882
|
"botocore[crt] >= 1.35.0",
|
|
4137
4883
|
{{#if (eq modelProvider "Bedrock")}}
|
|
4138
4884
|
"langchain-aws >= 1.0.0",
|
|
@@ -4145,6 +4891,7 @@ dependencies = [
|
|
|
4145
4891
|
{{/if}}
|
|
4146
4892
|
{{#if (eq modelProvider "Gemini")}}
|
|
4147
4893
|
"langchain-google-genai >= 3.0.3",
|
|
4894
|
+
"google-genai >= 1.0.0, < 2.0.0",
|
|
4148
4895
|
{{/if}}
|
|
4149
4896
|
{{#if hasGateway}}{{#if (includes gatewayAuthTypes "AWS_IAM")}}"mcp-proxy-for-aws >= 1.1.0",
|
|
4150
4897
|
{{/if}}{{/if}}
|
|
@@ -4171,6 +4918,11 @@ file defines a Starlette ASGI app with the OpenAI Agents SDK framework running w
|
|
|
4171
4918
|
|
|
4172
4919
|
\`model/load.py\` instantiates your chosen model provider (OpenAI).
|
|
4173
4920
|
|
|
4921
|
+
## Input Validation
|
|
4922
|
+
|
|
4923
|
+
Validate invocation input before forwarding it to the agent framework. Keep plain prompts typed as strings and pass
|
|
4924
|
+
only prompt text to the agent.
|
|
4925
|
+
|
|
4174
4926
|
## Environment Variables
|
|
4175
4927
|
|
|
4176
4928
|
| Variable | Required | Description |
|
|
@@ -4182,7 +4934,8 @@ file defines a Starlette ASGI app with the OpenAI Agents SDK framework running w
|
|
|
4182
4934
|
|
|
4183
4935
|
If installation was successful, a virtual environment is already created with dependencies installed.
|
|
4184
4936
|
|
|
4185
|
-
|
|
4937
|
+
Activate the environment with \`source .venv/bin/activate\` on macOS/Linux, \`.venv\\Scripts\\activate.bat\` in Windows
|
|
4938
|
+
Command Prompt, or \`.\\.venv\\Scripts\\activate.ps1\` in Windows PowerShell.
|
|
4186
4939
|
|
|
4187
4940
|
\`agentcore dev\` will start a local server on 0.0.0.0:8080.
|
|
4188
4941
|
|
|
@@ -4244,8 +4997,14 @@ Thumbs.db
|
|
|
4244
4997
|
`;
|
|
4245
4998
|
|
|
4246
4999
|
exports[`Assets Directory Snapshots > Python framework assets > python/python/http/openaiagents/base/main.py should match snapshot 1`] = `
|
|
4247
|
-
"
|
|
4248
|
-
|
|
5000
|
+
"{{#if needsOs}}
|
|
5001
|
+
import os
|
|
5002
|
+
{{/if}}
|
|
5003
|
+
{{#if hasGateway}}
|
|
5004
|
+
from contextlib import AsyncExitStack
|
|
5005
|
+
{{/if}}
|
|
5006
|
+
from functools import lru_cache
|
|
5007
|
+
from agents import Agent, Runner, SQLiteSession, function_tool
|
|
4249
5008
|
from bedrock_agentcore.runtime import BedrockAgentCoreApp
|
|
4250
5009
|
from model.load import load_model
|
|
4251
5010
|
{{#if hasGateway}}
|
|
@@ -4283,19 +5042,22 @@ def add_numbers(a: int, b: int) -> int:
|
|
|
4283
5042
|
|
|
4284
5043
|
tools = [add_numbers]
|
|
4285
5044
|
|
|
4286
|
-
{{#if
|
|
4287
|
-
|
|
5045
|
+
{{#if needsOs}}
|
|
5046
|
+
_MOUNT_PATHS = [
|
|
5047
|
+
{{#if sessionStorageMountPath}}"{{sessionStorageMountPath}}",{{/if}}
|
|
5048
|
+
{{#each efsMounts}}"{{mountPath}}",{{/each}}
|
|
5049
|
+
{{#each s3Mounts}}"{{mountPath}}",{{/each}}
|
|
5050
|
+
]
|
|
4288
5051
|
|
|
4289
5052
|
def _safe_resolve(path: str) -> str:
|
|
4290
|
-
|
|
4291
|
-
resolved
|
|
4292
|
-
|
|
4293
|
-
raise ValueError(f"Path '{path}' is outside the storage boundary")
|
|
5053
|
+
resolved = os.path.realpath(path)
|
|
5054
|
+
if not any(resolved == os.path.realpath(m) or resolved.startswith(os.path.realpath(m) + os.sep) for m in _MOUNT_PATHS):
|
|
5055
|
+
raise ValueError(f"Path '{path}' is not within any configured mount ({', '.join(_MOUNT_PATHS)})")
|
|
4294
5056
|
return resolved
|
|
4295
5057
|
|
|
4296
5058
|
@function_tool
|
|
4297
5059
|
def file_read(path: str) -> str:
|
|
4298
|
-
"""Read a file from
|
|
5060
|
+
"""Read a file from a mounted filesystem. Use the absolute path (e.g. /mnt/tools/data.txt)."""
|
|
4299
5061
|
try:
|
|
4300
5062
|
full_path = _safe_resolve(path)
|
|
4301
5063
|
with open(full_path) as f:
|
|
@@ -4307,7 +5069,7 @@ def file_read(path: str) -> str:
|
|
|
4307
5069
|
|
|
4308
5070
|
@function_tool
|
|
4309
5071
|
def file_write(path: str, content: str) -> str:
|
|
4310
|
-
"""Write
|
|
5072
|
+
"""Write a file to a mounted filesystem. Use the absolute path (e.g. /mnt/tools/data.txt)."""
|
|
4311
5073
|
try:
|
|
4312
5074
|
full_path = _safe_resolve(path)
|
|
4313
5075
|
parent = os.path.dirname(full_path)
|
|
@@ -4322,42 +5084,60 @@ def file_write(path: str, content: str) -> str:
|
|
|
4322
5084
|
return f"Error writing '{path}': {e.strerror}"
|
|
4323
5085
|
|
|
4324
5086
|
@function_tool
|
|
4325
|
-
def list_files(
|
|
4326
|
-
"""List files in
|
|
5087
|
+
def list_files(path: str) -> str:
|
|
5088
|
+
"""List files in a mounted filesystem directory. Use the absolute path (e.g. /mnt/tools)."""
|
|
4327
5089
|
try:
|
|
4328
|
-
|
|
4329
|
-
entries = os.listdir(
|
|
5090
|
+
full_path = _safe_resolve(path)
|
|
5091
|
+
entries = os.listdir(full_path)
|
|
4330
5092
|
return "\\n".join(entries) if entries else "(empty directory)"
|
|
4331
5093
|
except ValueError as e:
|
|
4332
5094
|
return str(e)
|
|
4333
5095
|
except OSError as e:
|
|
4334
|
-
return f"Error listing '{
|
|
5096
|
+
return f"Error listing '{path}': {e.strerror}"
|
|
4335
5097
|
|
|
4336
5098
|
tools.extend([file_read, file_write, list_files])
|
|
4337
5099
|
{{/if}}
|
|
4338
5100
|
|
|
4339
5101
|
INSTRUCTIONS = """
|
|
4340
5102
|
You are a helpful assistant. Use tools when appropriate.
|
|
4341
|
-
{{#if
|
|
4342
|
-
You have
|
|
4343
|
-
{{
|
|
5103
|
+
{{#if needsOs}}
|
|
5104
|
+
You have access to the following mounted filesystems. Use file_read, file_write, and list_files with full absolute paths:
|
|
5105
|
+
{{#if sessionStorageMountPath}}- {{sessionStorageMountPath}}: ephemeral session storage (lost when session ends)
|
|
5106
|
+
{{/if}}{{#each efsMounts}}- {{mountPath}}: EFS persistent storage (persists across sessions and agent restarts)
|
|
5107
|
+
{{/each}}{{#each s3Mounts}}- {{mountPath}}: S3 Files persistent storage (durable, backed by S3)
|
|
5108
|
+
{{/each}}{{/if}}
|
|
4344
5109
|
"""
|
|
4345
5110
|
|
|
5111
|
+
# Caches up to 128 active sessions; LRU eviction silently resets history for
|
|
5112
|
+
# the oldest session. For production use, replace with a durable session store
|
|
5113
|
+
# (e.g. SQLiteSession with a file path).
|
|
5114
|
+
@lru_cache(maxsize=128)
|
|
5115
|
+
def get_session(session_id):
|
|
5116
|
+
return SQLiteSession(session_id)
|
|
5117
|
+
|
|
5118
|
+
|
|
4346
5119
|
# Define the agent execution
|
|
4347
|
-
async def main(query):
|
|
5120
|
+
async def main(query, session):
|
|
4348
5121
|
ensure_credentials_loaded()
|
|
4349
5122
|
try:
|
|
4350
5123
|
{{#if hasGateway}}
|
|
4351
5124
|
if mcp_servers:
|
|
4352
|
-
|
|
4353
|
-
|
|
4354
|
-
|
|
4355
|
-
|
|
4356
|
-
mcp_servers
|
|
4357
|
-
|
|
4358
|
-
|
|
4359
|
-
|
|
4360
|
-
|
|
5125
|
+
# Connect every gateway server before the run; include_server_in_tool_names
|
|
5126
|
+
# namespaces each server's tools so multiple gateways exposing the same tool
|
|
5127
|
+
# (e.g. x_amz_bedrock_agentcore_search) don't collide.
|
|
5128
|
+
async with AsyncExitStack() as stack:
|
|
5129
|
+
for server in mcp_servers:
|
|
5130
|
+
await stack.enter_async_context(server)
|
|
5131
|
+
agent = Agent(
|
|
5132
|
+
name="{{ name }}",
|
|
5133
|
+
model="gpt-4.1",
|
|
5134
|
+
instructions=INSTRUCTIONS,
|
|
5135
|
+
mcp_servers=mcp_servers,
|
|
5136
|
+
tools=tools,
|
|
5137
|
+
mcp_config={"include_server_in_tool_names": True},
|
|
5138
|
+
)
|
|
5139
|
+
result = await Runner.run(agent, query, session=session)
|
|
5140
|
+
return result
|
|
4361
5141
|
else:
|
|
4362
5142
|
agent = Agent(
|
|
4363
5143
|
name="{{ name }}",
|
|
@@ -4366,7 +5146,7 @@ async def main(query):
|
|
|
4366
5146
|
mcp_servers=[],
|
|
4367
5147
|
tools=tools
|
|
4368
5148
|
)
|
|
4369
|
-
result = await Runner.run(agent, query)
|
|
5149
|
+
result = await Runner.run(agent, query, session=session)
|
|
4370
5150
|
return result
|
|
4371
5151
|
{{else}}
|
|
4372
5152
|
if mcp_servers:
|
|
@@ -4379,7 +5159,7 @@ async def main(query):
|
|
|
4379
5159
|
mcp_servers=active_servers,
|
|
4380
5160
|
tools=tools
|
|
4381
5161
|
)
|
|
4382
|
-
result = await Runner.run(agent, query)
|
|
5162
|
+
result = await Runner.run(agent, query, session=session)
|
|
4383
5163
|
return result
|
|
4384
5164
|
else:
|
|
4385
5165
|
agent = Agent(
|
|
@@ -4389,7 +5169,7 @@ async def main(query):
|
|
|
4389
5169
|
mcp_servers=[],
|
|
4390
5170
|
tools=tools
|
|
4391
5171
|
)
|
|
4392
|
-
result = await Runner.run(agent, query)
|
|
5172
|
+
result = await Runner.run(agent, query, session=session)
|
|
4393
5173
|
return result
|
|
4394
5174
|
{{/if}}
|
|
4395
5175
|
except Exception as e:
|
|
@@ -4403,9 +5183,13 @@ async def invoke(payload, context):
|
|
|
4403
5183
|
|
|
4404
5184
|
# Process the user prompt
|
|
4405
5185
|
prompt = payload.get("prompt", "What can you help me with?")
|
|
5186
|
+
if not isinstance(prompt, str):
|
|
5187
|
+
raise ValueError("prompt must be a string")
|
|
5188
|
+
session_id = getattr(context, "session_id", "default-session")
|
|
5189
|
+
session = get_session(session_id)
|
|
4406
5190
|
|
|
4407
|
-
# Run the agent
|
|
4408
|
-
result = await main(prompt)
|
|
5191
|
+
# Run the agent (session automatically loads/saves conversation history)
|
|
5192
|
+
result = await main(prompt, session)
|
|
4409
5193
|
|
|
4410
5194
|
# Return result
|
|
4411
5195
|
return {"result": result.final_output}
|
|
@@ -4457,20 +5241,20 @@ def get_all_gateway_mcp_servers() -> list[MCPServerStreamableHttp]:
|
|
|
4457
5241
|
{{#each gatewayProviders}}
|
|
4458
5242
|
url = os.environ.get("{{envVarName}}")
|
|
4459
5243
|
if url:
|
|
4460
|
-
|
|
5244
|
+
{{#if (eq authType "AWS_IAM")}}
|
|
4461
5245
|
session = create_aws_session()
|
|
4462
5246
|
auth = SigV4HTTPXAuth(session.get_credentials(), "bedrock-agentcore", session.region_name)
|
|
4463
5247
|
servers.append(MCPServerStreamableHttp(
|
|
4464
5248
|
name="{{name}}",
|
|
4465
5249
|
params={"url": url, "httpx_client_factory": lambda **kwargs: httpx.AsyncClient(auth=auth, **kwargs)}
|
|
4466
5250
|
))
|
|
4467
|
-
|
|
5251
|
+
{{else if (eq authType "CUSTOM_JWT")}}
|
|
4468
5252
|
token = _get_bearer_token_{{snakeCase name}}()
|
|
4469
5253
|
headers = {"Authorization": f"Bearer {token}"} if token else {}
|
|
4470
5254
|
servers.append(MCPServerStreamableHttp(name="{{name}}", params={"url": url, "headers": headers}))
|
|
4471
|
-
|
|
5255
|
+
{{else}}
|
|
4472
5256
|
servers.append(MCPServerStreamableHttp(name="{{name}}", params={"url": url}))
|
|
4473
|
-
|
|
5257
|
+
{{/if}}
|
|
4474
5258
|
else:
|
|
4475
5259
|
logger.warning("{{envVarName}} not set — {{name}} gateway tools unavailable")
|
|
4476
5260
|
{{/each}}
|
|
@@ -4557,9 +5341,9 @@ description = "AgentCore Runtime Application using OpenAI Agents SDK"
|
|
|
4557
5341
|
readme = "README.md"
|
|
4558
5342
|
requires-python = ">=3.10"
|
|
4559
5343
|
dependencies = [
|
|
4560
|
-
"aws-opentelemetry-distro",
|
|
4561
|
-
"openai-agents >= 0.
|
|
4562
|
-
"bedrock-agentcore >= 1.0
|
|
5344
|
+
"aws-opentelemetry-distro ~= 0.18.0",
|
|
5345
|
+
"openai-agents >= 0.16.0",
|
|
5346
|
+
"bedrock-agentcore >= 1.8.0",
|
|
4563
5347
|
"botocore[crt] >= 1.35.0",
|
|
4564
5348
|
{{#if hasGateway}}{{#if (includes gatewayAuthTypes "AWS_IAM")}}"mcp-proxy-for-aws >= 1.1.0",
|
|
4565
5349
|
{{/if}}{{/if}}
|
|
@@ -4586,6 +5370,12 @@ file defines a Starlette ASGI app with the chosen Agent framework SDK running wi
|
|
|
4586
5370
|
|
|
4587
5371
|
\`model/load.py\` instantiates your chosen model provider.
|
|
4588
5372
|
|
|
5373
|
+
## Input Validation
|
|
5374
|
+
|
|
5375
|
+
Validate invocation input before forwarding it to Strands. Keep plain prompts typed as strings. If the app accepts a
|
|
5376
|
+
caller-supplied message history, retain \`strip_trailing_tool_use()\`, which normalizes the history tail before
|
|
5377
|
+
invoking the agent.
|
|
5378
|
+
|
|
4589
5379
|
## Environment Variables
|
|
4590
5380
|
|
|
4591
5381
|
| Variable | Required | Description |
|
|
@@ -4597,7 +5387,8 @@ file defines a Starlette ASGI app with the chosen Agent framework SDK running wi
|
|
|
4597
5387
|
|
|
4598
5388
|
If installation was successful, a virtual environment is already created with dependencies installed.
|
|
4599
5389
|
|
|
4600
|
-
|
|
5390
|
+
Activate the environment with \`source .venv/bin/activate\` on macOS/Linux, \`.venv\\Scripts\\activate.bat\` in Windows
|
|
5391
|
+
Command Prompt, or \`.\\.venv\\Scripts\\activate.ps1\` in Windows PowerShell.
|
|
4601
5392
|
|
|
4602
5393
|
\`agentcore dev\` will start a local server on 0.0.0.0:8080.
|
|
4603
5394
|
|
|
@@ -4658,54 +5449,293 @@ Thumbs.db"
|
|
|
4658
5449
|
`;
|
|
4659
5450
|
|
|
4660
5451
|
exports[`Assets Directory Snapshots > Python framework assets > python/python/http/strands/base/main.py should match snapshot 1`] = `
|
|
4661
|
-
"from
|
|
5452
|
+
"from typing import Any
|
|
5453
|
+
from collections import OrderedDict
|
|
5454
|
+
{{#if inlineFunctionTools}}
|
|
5455
|
+
import json
|
|
5456
|
+
|
|
5457
|
+
from strands.tools.tools import PythonAgentTool
|
|
5458
|
+
from strands.types.tools import ToolResult, ToolUse
|
|
5459
|
+
{{/if}}
|
|
5460
|
+
from strands import Agent, tool
|
|
5461
|
+
{{#if hasSkillsFetcher}}
|
|
5462
|
+
from strands import AgentSkills
|
|
5463
|
+
{{#if hasFetchedSkills}}
|
|
5464
|
+
from skills.fetcher import resolve_s3_skills, resolve_git_skills
|
|
5465
|
+
{{/if}}
|
|
5466
|
+
{{#if (some gitSkills "credentialArn")}}
|
|
5467
|
+
from bedrock_agentcore.services.identity import IdentityClient
|
|
5468
|
+
{{/if}}
|
|
5469
|
+
{{/if}}
|
|
5470
|
+
import asyncio
|
|
5471
|
+
{{#if hasShell}}
|
|
5472
|
+
import subprocess
|
|
5473
|
+
{{/if}}
|
|
5474
|
+
{{#if hasFileOperations}}
|
|
5475
|
+
import os
|
|
5476
|
+
{{/if}}
|
|
5477
|
+
{{#if hasExecutionLimits}}
|
|
5478
|
+
from strands.tools.executors import SequentialToolExecutor
|
|
5479
|
+
from strands.types.exceptions import EventLoopException
|
|
5480
|
+
from hooks.execution_limits import ExecutionLimitExceeded, ExecutionLimitsHook
|
|
5481
|
+
{{/if}}
|
|
5482
|
+
{{#if hasConfigBundle}}
|
|
5483
|
+
from strands.hooks import HookProvider, HookRegistry, BeforeInvocationEvent, BeforeToolCallEvent
|
|
5484
|
+
{{/if}}
|
|
5485
|
+
{{#if truncationStrategy}}
|
|
5486
|
+
{{#if (eq truncationStrategy "sliding_window")}}
|
|
5487
|
+
from strands.agent.conversation_manager.sliding_window_conversation_manager import SlidingWindowConversationManager
|
|
5488
|
+
{{/if}}
|
|
5489
|
+
{{#if (eq truncationStrategy "summarization")}}
|
|
5490
|
+
from strands.agent.conversation_manager.summarizing_conversation_manager import SummarizingConversationManager
|
|
5491
|
+
{{/if}}
|
|
5492
|
+
{{else}}
|
|
5493
|
+
from strands.agent.conversation_manager.null_conversation_manager import NullConversationManager
|
|
5494
|
+
{{/if}}
|
|
5495
|
+
{{#if hasConfigBundle}}
|
|
5496
|
+
from bedrock_agentcore.runtime.context import BedrockAgentCoreContext
|
|
5497
|
+
{{/if}}
|
|
5498
|
+
{{#if hasBrowser}}
|
|
5499
|
+
from strands_tools.browser import AgentCoreBrowser
|
|
5500
|
+
{{/if}}
|
|
5501
|
+
{{#if hasCodeInterpreter}}
|
|
5502
|
+
from strands_tools.code_interpreter import AgentCoreCodeInterpreter
|
|
5503
|
+
{{/if}}
|
|
4662
5504
|
from bedrock_agentcore.runtime import BedrockAgentCoreApp
|
|
4663
5505
|
from model.load import load_model
|
|
4664
5506
|
{{#if hasGateway}}
|
|
4665
5507
|
from mcp_client.client import get_all_gateway_mcp_clients
|
|
4666
|
-
{{else}}
|
|
4667
|
-
from mcp_client.client import get_streamable_http_mcp_client
|
|
4668
5508
|
{{/if}}
|
|
5509
|
+
{{#if remoteMcpTools}}
|
|
5510
|
+
from mcp_client.client import get_all_remote_mcp_clients
|
|
5511
|
+
{{/if}}
|
|
5512
|
+
{{#unless (or hasGateway remoteMcpTools)}}
|
|
5513
|
+
{{#unless isExportHarness}}
|
|
5514
|
+
from mcp_client.client import get_streamable_http_mcp_client
|
|
5515
|
+
{{/unless}}
|
|
5516
|
+
{{/unless}}
|
|
4669
5517
|
{{#if hasMemory}}
|
|
4670
5518
|
from memory.session import get_memory_session_manager
|
|
4671
5519
|
{{/if}}
|
|
4672
|
-
{{#
|
|
5520
|
+
{{#unless hasFileOperations}}
|
|
5521
|
+
{{#if (or needsOs browserIdentifierEnvVar codeInterpreterIdentifierEnvVar (some gitSkills "credentialArn"))}}
|
|
4673
5522
|
import os
|
|
4674
5523
|
{{/if}}
|
|
5524
|
+
{{/unless}}
|
|
5525
|
+
{{#if hasPayment}}
|
|
5526
|
+
from capabilities.payments.payments import create_payments_plugin, PAYMENT_SYSTEM_PROMPT
|
|
5527
|
+
{{/if}}
|
|
4675
5528
|
|
|
4676
5529
|
app = BedrockAgentCoreApp()
|
|
4677
5530
|
log = app.logger
|
|
4678
5531
|
|
|
4679
|
-
#
|
|
5532
|
+
{{#if (or hasGateway remoteMcpTools)}}
|
|
5533
|
+
# Define MCP clients for all configured MCP servers (gateways and/or remote MCP)
|
|
5534
|
+
mcp_clients = []
|
|
4680
5535
|
{{#if hasGateway}}
|
|
4681
|
-
mcp_clients
|
|
5536
|
+
mcp_clients += get_all_gateway_mcp_clients()
|
|
5537
|
+
{{/if}}
|
|
5538
|
+
{{#if remoteMcpTools}}
|
|
5539
|
+
mcp_clients += get_all_remote_mcp_clients()
|
|
5540
|
+
{{/if}}
|
|
4682
5541
|
{{else}}
|
|
5542
|
+
{{#unless isExportHarness}}
|
|
5543
|
+
# Define a Streamable HTTP MCP Client
|
|
4683
5544
|
mcp_clients = [get_streamable_http_mcp_client()]
|
|
5545
|
+
{{/unless}}
|
|
5546
|
+
{{/if}}
|
|
5547
|
+
|
|
5548
|
+
{{#if systemPromptText}}
|
|
5549
|
+
DEFAULT_SYSTEM_PROMPT = """{{escapePyStr systemPromptText}}"""
|
|
5550
|
+
{{else}}
|
|
5551
|
+
DEFAULT_SYSTEM_PROMPT = """
|
|
5552
|
+
You are a helpful assistant. Use tools when appropriate.
|
|
5553
|
+
{{#if needsOs}}{{#unless isExportHarness}}
|
|
5554
|
+
You have access to the following mounted filesystems. Use file_read, file_write, and list_files with full absolute paths:
|
|
5555
|
+
{{#if sessionStorageMountPath}}- {{sessionStorageMountPath}}: ephemeral session storage (lost when session ends)
|
|
5556
|
+
{{/if}}{{#each efsMounts}}- {{mountPath}}: EFS persistent storage (persists across sessions and agent restarts)
|
|
5557
|
+
{{/each}}{{#each s3Mounts}}- {{mountPath}}: S3 Files persistent storage (durable, backed by S3)
|
|
5558
|
+
{{/each}}{{/unless}}{{/if}}
|
|
5559
|
+
"""
|
|
5560
|
+
{{/if}}
|
|
5561
|
+
|
|
5562
|
+
{{#if hasConfigBundle}}
|
|
5563
|
+
DEFAULT_TOOL_DESC = "Return the sum of two numbers"
|
|
4684
5564
|
{{/if}}
|
|
4685
5565
|
|
|
4686
5566
|
# Define a collection of tools used by the model
|
|
4687
5567
|
tools = []
|
|
4688
5568
|
|
|
5569
|
+
{{#if inlineFunctionTools}}
|
|
5570
|
+
# Inline function tools — stop the agent loop so the tool call streams back to the caller
|
|
5571
|
+
def _make_inline_tool(name: str, spec: dict) -> PythonAgentTool:
|
|
5572
|
+
def _handler(tool: ToolUse, **kwargs: Any) -> ToolResult:
|
|
5573
|
+
kwargs.get("request_state", {})["stop_event_loop"] = True
|
|
5574
|
+
return {"toolUseId": tool["toolUseId"], "status": "success", "content": [{"text": " "}]}
|
|
5575
|
+
_handler.__name__ = name
|
|
5576
|
+
return PythonAgentTool(tool_name=name, tool_spec=spec, tool_func=_handler)
|
|
5577
|
+
|
|
5578
|
+
{{#each inlineFunctionTools}}
|
|
5579
|
+
_INLINE_SPEC_{{snakeCase name}} = {
|
|
5580
|
+
"name": "{{name}}",
|
|
5581
|
+
"description": {{safeJson description}},
|
|
5582
|
+
"inputSchema": {"json": json.loads({{pyJsonStr inputSchema}}) },
|
|
5583
|
+
}
|
|
5584
|
+
tools.append(_make_inline_tool("{{name}}", _INLINE_SPEC_{{snakeCase name}}))
|
|
5585
|
+
{{/each}}
|
|
5586
|
+
|
|
5587
|
+
_INLINE_FUNCTION_NAMES = { {{#each inlineFunctionTools}}"{{name}}"{{#unless @last}}, {{/unless}}{{/each}} }
|
|
5588
|
+
|
|
5589
|
+
{{else}}
|
|
5590
|
+
_INLINE_FUNCTION_NAMES = set()
|
|
5591
|
+
|
|
5592
|
+
{{#unless isExportHarness}}
|
|
4689
5593
|
# Define a simple function tool
|
|
5594
|
+
{{#if hasConfigBundle}}
|
|
5595
|
+
@tool(description=DEFAULT_TOOL_DESC)
|
|
5596
|
+
{{else}}
|
|
4690
5597
|
@tool
|
|
5598
|
+
{{/if}}
|
|
4691
5599
|
def add_numbers(a: int, b: int) -> int:
|
|
4692
5600
|
"""Return the sum of two numbers"""
|
|
4693
5601
|
return a+b
|
|
4694
5602
|
tools.append(add_numbers)
|
|
4695
5603
|
|
|
4696
|
-
{{
|
|
4697
|
-
|
|
5604
|
+
{{/unless}}
|
|
5605
|
+
{{/if}}
|
|
5606
|
+
{{#if hasBrowser}}
|
|
5607
|
+
{{#if browserIdentifierEnvVar}}
|
|
5608
|
+
_browser_id = os.getenv("{{browserIdentifierEnvVar}}")
|
|
5609
|
+
tools.append(AgentCoreBrowser(**({"identifier": _browser_id} if _browser_id else {})).browser)
|
|
5610
|
+
{{else}}
|
|
5611
|
+
tools.append(AgentCoreBrowser().browser)
|
|
5612
|
+
{{/if}}
|
|
5613
|
+
{{/if}}
|
|
5614
|
+
{{#if hasCodeInterpreter}}
|
|
5615
|
+
{{#if codeInterpreterIdentifierEnvVar}}
|
|
5616
|
+
_code_interpreter_id = os.getenv("{{codeInterpreterIdentifierEnvVar}}")
|
|
5617
|
+
tools.append(AgentCoreCodeInterpreter(**({"identifier": _code_interpreter_id} if _code_interpreter_id else {})).code_interpreter)
|
|
5618
|
+
{{else}}
|
|
5619
|
+
tools.append(AgentCoreCodeInterpreter().code_interpreter)
|
|
5620
|
+
{{/if}}
|
|
5621
|
+
{{/if}}
|
|
5622
|
+
{{#if hasShell}}
|
|
5623
|
+
@tool
|
|
5624
|
+
def shell(command: str, timeout: int = 300) -> dict:
|
|
5625
|
+
"""Execute a bash command and return the results.
|
|
5626
|
+
|
|
5627
|
+
Args:
|
|
5628
|
+
command: The bash command to execute
|
|
5629
|
+
timeout: Timeout in seconds (default: 300)
|
|
5630
|
+
|
|
5631
|
+
Returns:
|
|
5632
|
+
Dict with stdout, stderr, and exit_code
|
|
5633
|
+
"""
|
|
5634
|
+
result = subprocess.run(
|
|
5635
|
+
command, shell=True, capture_output=True, text=True, timeout=timeout
|
|
5636
|
+
)
|
|
5637
|
+
return {"stdout": result.stdout, "stderr": result.stderr, "exit_code": result.returncode}
|
|
5638
|
+
|
|
5639
|
+
tools.append(shell)
|
|
5640
|
+
{{/if}}
|
|
5641
|
+
{{#if hasFileOperations}}
|
|
5642
|
+
@tool
|
|
5643
|
+
def file_operations(
|
|
5644
|
+
command: str,
|
|
5645
|
+
path: str,
|
|
5646
|
+
old_str: str = None,
|
|
5647
|
+
new_str: str = None,
|
|
5648
|
+
file_text: str = None,
|
|
5649
|
+
insert_line: int = None,
|
|
5650
|
+
view_range: list = None,
|
|
5651
|
+
) -> str:
|
|
5652
|
+
"""Text editor tool for viewing and modifying files.
|
|
5653
|
+
|
|
5654
|
+
Args:
|
|
5655
|
+
command: The command to execute ("view", "str_replace", "create", "insert")
|
|
5656
|
+
path: Path to the file or directory
|
|
5657
|
+
old_str: Text to replace (for str_replace command)
|
|
5658
|
+
new_str: Replacement text (for str_replace and insert commands)
|
|
5659
|
+
file_text: Content for new file (for create command)
|
|
5660
|
+
insert_line: Line number to insert after (for insert command)
|
|
5661
|
+
view_range: [start_line, end_line] for viewing specific lines (for view command)
|
|
5662
|
+
|
|
5663
|
+
Returns:
|
|
5664
|
+
Result of the operation
|
|
5665
|
+
"""
|
|
5666
|
+
try:
|
|
5667
|
+
if command == "view":
|
|
5668
|
+
if not os.path.exists(path):
|
|
5669
|
+
return f"Error: Path '{path}' does not exist"
|
|
5670
|
+
if os.path.isdir(path):
|
|
5671
|
+
return "\\n".join(os.listdir(path))
|
|
5672
|
+
with open(path) as f:
|
|
5673
|
+
lines = f.read().splitlines()
|
|
5674
|
+
if view_range:
|
|
5675
|
+
start, end = view_range
|
|
5676
|
+
start_idx = max(0, start - 1)
|
|
5677
|
+
end_idx = len(lines) if end == -1 else min(len(lines), end)
|
|
5678
|
+
lines = lines[start_idx:end_idx]
|
|
5679
|
+
start_num = start_idx + 1
|
|
5680
|
+
else:
|
|
5681
|
+
start_num = 1
|
|
5682
|
+
return "\\n".join(f"{start_num + i}: {line}" for i, line in enumerate(lines))
|
|
5683
|
+
elif command == "str_replace":
|
|
5684
|
+
if old_str is None or new_str is None:
|
|
5685
|
+
return "Error: str_replace requires both old_str and new_str parameters"
|
|
5686
|
+
if not os.path.exists(path):
|
|
5687
|
+
return f"Error: File '{path}' does not exist"
|
|
5688
|
+
content = open(path).read()
|
|
5689
|
+
if old_str not in content:
|
|
5690
|
+
return "Error: Text not found in file"
|
|
5691
|
+
count = content.count(old_str)
|
|
5692
|
+
if count > 1:
|
|
5693
|
+
return f"Error: Text appears {count} times in file. Please be more specific."
|
|
5694
|
+
open(path, "w").write(content.replace(old_str, new_str, 1))
|
|
5695
|
+
return f"Successfully replaced text in '{path}'"
|
|
5696
|
+
elif command == "create":
|
|
5697
|
+
if file_text is None:
|
|
5698
|
+
return "Error: create requires file_text parameter"
|
|
5699
|
+
os.makedirs(os.path.dirname(os.path.abspath(path)), exist_ok=True)
|
|
5700
|
+
open(path, "w").write(file_text)
|
|
5701
|
+
return f"Successfully created file '{path}'"
|
|
5702
|
+
elif command == "insert":
|
|
5703
|
+
if new_str is None or insert_line is None:
|
|
5704
|
+
return "Error: insert requires both new_str and insert_line parameters"
|
|
5705
|
+
if not os.path.exists(path):
|
|
5706
|
+
return f"Error: File '{path}' does not exist"
|
|
5707
|
+
lines = open(path).read().splitlines(True)
|
|
5708
|
+
if insert_line == 0:
|
|
5709
|
+
lines.insert(0, new_str + "\\n")
|
|
5710
|
+
elif insert_line >= len(lines):
|
|
5711
|
+
lines.append(new_str + "\\n")
|
|
5712
|
+
else:
|
|
5713
|
+
lines.insert(insert_line, new_str + "\\n")
|
|
5714
|
+
open(path, "w").write("".join(lines))
|
|
5715
|
+
return f"Successfully inserted text in '{path}' at line {insert_line + 1}"
|
|
5716
|
+
else:
|
|
5717
|
+
return f"Error: Unknown command '{command}'"
|
|
5718
|
+
except Exception as e:
|
|
5719
|
+
return f"Error: {e}"
|
|
5720
|
+
|
|
5721
|
+
tools.append(file_operations)
|
|
5722
|
+
{{/if}}
|
|
5723
|
+
{{#if needsOs}}{{#unless isExportHarness}}
|
|
5724
|
+
_MOUNT_PATHS = [
|
|
5725
|
+
{{#if sessionStorageMountPath}}"{{sessionStorageMountPath}}",{{/if}}
|
|
5726
|
+
{{#each efsMounts}}"{{mountPath}}",{{/each}}
|
|
5727
|
+
{{#each s3Mounts}}"{{mountPath}}",{{/each}}
|
|
5728
|
+
]
|
|
4698
5729
|
|
|
4699
5730
|
def _safe_resolve(path: str) -> str:
|
|
4700
|
-
|
|
4701
|
-
resolved
|
|
4702
|
-
|
|
4703
|
-
raise ValueError(f"Path '{path}' is outside the storage boundary")
|
|
5731
|
+
resolved = os.path.realpath(path)
|
|
5732
|
+
if not any(resolved == os.path.realpath(m) or resolved.startswith(os.path.realpath(m) + os.sep) for m in _MOUNT_PATHS):
|
|
5733
|
+
raise ValueError(f"Path '{path}' is not within any configured mount ({', '.join(_MOUNT_PATHS)})")
|
|
4704
5734
|
return resolved
|
|
4705
5735
|
|
|
4706
5736
|
@tool
|
|
4707
5737
|
def file_read(path: str) -> str:
|
|
4708
|
-
"""Read a file from
|
|
5738
|
+
"""Read a file from a mounted filesystem. Use the absolute path (e.g. /mnt/tools/data.txt)."""
|
|
4709
5739
|
try:
|
|
4710
5740
|
full_path = _safe_resolve(path)
|
|
4711
5741
|
with open(full_path) as f:
|
|
@@ -4717,7 +5747,7 @@ def file_read(path: str) -> str:
|
|
|
4717
5747
|
|
|
4718
5748
|
@tool
|
|
4719
5749
|
def file_write(path: str, content: str) -> str:
|
|
4720
|
-
"""Write
|
|
5750
|
+
"""Write a file to a mounted filesystem. Use the absolute path (e.g. /mnt/tools/data.txt)."""
|
|
4721
5751
|
try:
|
|
4722
5752
|
full_path = _safe_resolve(path)
|
|
4723
5753
|
parent = os.path.dirname(full_path)
|
|
@@ -4732,82 +5762,403 @@ def file_write(path: str, content: str) -> str:
|
|
|
4732
5762
|
return f"Error writing '{path}': {e.strerror}"
|
|
4733
5763
|
|
|
4734
5764
|
@tool
|
|
4735
|
-
def list_files(
|
|
4736
|
-
"""List files in
|
|
5765
|
+
def list_files(path: str) -> str:
|
|
5766
|
+
"""List files in a mounted filesystem directory. Use the absolute path (e.g. /mnt/tools)."""
|
|
4737
5767
|
try:
|
|
4738
|
-
|
|
4739
|
-
entries = os.listdir(
|
|
5768
|
+
full_path = _safe_resolve(path)
|
|
5769
|
+
entries = os.listdir(full_path)
|
|
4740
5770
|
return "\\n".join(entries) if entries else "(empty directory)"
|
|
4741
5771
|
except ValueError as e:
|
|
4742
5772
|
return str(e)
|
|
4743
5773
|
except OSError as e:
|
|
4744
|
-
return f"Error listing '{
|
|
5774
|
+
return f"Error listing '{path}': {e.strerror}"
|
|
4745
5775
|
|
|
4746
5776
|
tools.extend([file_read, file_write, list_files])
|
|
4747
|
-
{{/if}}
|
|
5777
|
+
{{/unless}}{{/if}}
|
|
4748
5778
|
|
|
5779
|
+
{{#if (or hasGateway remoteMcpTools)}}
|
|
5780
|
+
# Add MCP clients to tools
|
|
5781
|
+
for mcp_client in mcp_clients:
|
|
5782
|
+
if mcp_client:
|
|
5783
|
+
tools.append(mcp_client)
|
|
5784
|
+
{{else}}
|
|
5785
|
+
{{#unless isExportHarness}}
|
|
4749
5786
|
# Add MCP client to tools if available
|
|
4750
5787
|
for mcp_client in mcp_clients:
|
|
4751
5788
|
if mcp_client:
|
|
4752
5789
|
tools.append(mcp_client)
|
|
5790
|
+
{{/unless}}
|
|
5791
|
+
{{/if}}
|
|
5792
|
+
|
|
5793
|
+
{{#if hasConfigBundle}}
|
|
5794
|
+
|
|
5795
|
+
class ConfigBundleHook(HookProvider):
|
|
5796
|
+
"""Injects config bundle values (system prompt, tool descriptions) before each invocation.
|
|
5797
|
+
|
|
5798
|
+
BedrockAgentCoreContext.get_config_bundle() fetches the component configuration
|
|
5799
|
+
for the current runtime ARN from the config bundle service. The SDK caches the
|
|
5800
|
+
result and refreshes on bundle version changes.
|
|
5801
|
+
"""
|
|
5802
|
+
|
|
5803
|
+
def register_hooks(self, registry: HookRegistry, **kwargs: Any) -> None:
|
|
5804
|
+
registry.add_callback(BeforeInvocationEvent, self._inject_system_prompt)
|
|
5805
|
+
registry.add_callback(BeforeToolCallEvent, self._override_tool_desc)
|
|
5806
|
+
|
|
5807
|
+
def _inject_system_prompt(self, event: BeforeInvocationEvent) -> None:
|
|
5808
|
+
config = BedrockAgentCoreContext.get_config_bundle()
|
|
5809
|
+
prompt = config.get("systemPrompt", DEFAULT_SYSTEM_PROMPT)
|
|
5810
|
+
|
|
5811
|
+
if prompt != event.agent.system_prompt:
|
|
5812
|
+
event.agent.system_prompt = prompt
|
|
5813
|
+
|
|
5814
|
+
def _override_tool_desc(self, event: BeforeToolCallEvent) -> None:
|
|
5815
|
+
config = BedrockAgentCoreContext.get_config_bundle()
|
|
5816
|
+
tool_descs = config.get("toolDescriptions", {})
|
|
5817
|
+
|
|
5818
|
+
tool_name = event.tool_use["name"]
|
|
5819
|
+
override = tool_descs.get(tool_name)
|
|
5820
|
+
if override and event.selected_tool:
|
|
5821
|
+
spec = event.selected_tool.tool_spec
|
|
5822
|
+
if spec and "description" in spec:
|
|
5823
|
+
spec["description"] = override
|
|
4753
5824
|
|
|
4754
|
-
SYSTEM_PROMPT = """
|
|
4755
|
-
You are a helpful assistant. Use tools when appropriate.
|
|
4756
|
-
{{#if sessionStorageMountPath}}
|
|
4757
|
-
You have persistent storage at {{sessionStorageMountPath}}. Use file tools to read and write files. Data persists across sessions.
|
|
4758
5825
|
{{/if}}
|
|
4759
|
-
|
|
5826
|
+
|
|
5827
|
+
def _make_conversation_manager():
|
|
5828
|
+
{{#if truncationStrategy}}
|
|
5829
|
+
{{#if (eq truncationStrategy "sliding_window")}}
|
|
5830
|
+
{{#if truncationConfig}}
|
|
5831
|
+
return SlidingWindowConversationManager(**{{safeJson truncationConfig}}, per_turn=True)
|
|
5832
|
+
{{else}}
|
|
5833
|
+
return SlidingWindowConversationManager(per_turn=True)
|
|
5834
|
+
{{/if}}
|
|
5835
|
+
{{else}}
|
|
5836
|
+
{{#if truncationConfig}}
|
|
5837
|
+
return SummarizingConversationManager(**{{safeJson truncationConfig}})
|
|
5838
|
+
{{else}}
|
|
5839
|
+
return SummarizingConversationManager()
|
|
5840
|
+
{{/if}}
|
|
5841
|
+
{{/if}}
|
|
5842
|
+
{{else}}
|
|
5843
|
+
return NullConversationManager()
|
|
5844
|
+
{{/if}}
|
|
4760
5845
|
|
|
4761
5846
|
{{#if hasMemory}}
|
|
5847
|
+
{{#unless hasPayment}}
|
|
4762
5848
|
def agent_factory():
|
|
4763
5849
|
cache = {}
|
|
4764
|
-
def get_or_create_agent(session_id, user_id):
|
|
4765
|
-
|
|
5850
|
+
def get_or_create_agent(session_id, user_id{{#if hasSkillsFetcher}}, skill_plugins=None{{/if}}):
|
|
5851
|
+
{{#if actorId}}
|
|
5852
|
+
_actor_id = "{{actorId}}"
|
|
5853
|
+
{{else}}
|
|
5854
|
+
_actor_id = user_id
|
|
5855
|
+
{{/if}}
|
|
5856
|
+
key = f"{session_id}/{_actor_id}"
|
|
4766
5857
|
if key not in cache:
|
|
4767
|
-
# Create an agent for the given session_id and user_id
|
|
4768
5858
|
cache[key] = Agent(
|
|
4769
5859
|
model=load_model(),
|
|
4770
|
-
session_manager=get_memory_session_manager(session_id,
|
|
4771
|
-
|
|
4772
|
-
|
|
5860
|
+
session_manager=get_memory_session_manager(session_id, _actor_id),
|
|
5861
|
+
conversation_manager=_make_conversation_manager(),
|
|
5862
|
+
system_prompt=DEFAULT_SYSTEM_PROMPT,
|
|
5863
|
+
tools=tools,
|
|
5864
|
+
{{#if hasSkillsFetcher}}
|
|
5865
|
+
plugins=skill_plugins or None,
|
|
5866
|
+
{{/if}}
|
|
5867
|
+
{{#if hasExecutionLimits}}
|
|
5868
|
+
tool_executor=SequentialToolExecutor(),
|
|
5869
|
+
callback_handler=None,
|
|
5870
|
+
{{/if}}
|
|
5871
|
+
hooks=[
|
|
5872
|
+
{{#if hasExecutionLimits}}
|
|
5873
|
+
ExecutionLimitsHook(
|
|
5874
|
+
{{#if maxIterations}}max_iterations={{maxIterations}},{{/if}}
|
|
5875
|
+
{{#if maxTokens}}max_tokens={{maxTokens}},{{/if}}
|
|
5876
|
+
{{#if timeoutSeconds}}timeout_seconds={{timeoutSeconds}},{{/if}}
|
|
5877
|
+
),
|
|
5878
|
+
{{/if}}
|
|
5879
|
+
{{#if hasConfigBundle}}
|
|
5880
|
+
ConfigBundleHook(),
|
|
5881
|
+
{{/if}}
|
|
5882
|
+
],
|
|
4773
5883
|
)
|
|
4774
5884
|
return cache[key]
|
|
4775
5885
|
return get_or_create_agent
|
|
4776
5886
|
get_or_create_agent = agent_factory()
|
|
5887
|
+
{{/unless}}
|
|
4777
5888
|
{{else}}
|
|
4778
|
-
|
|
4779
|
-
|
|
4780
|
-
|
|
4781
|
-
|
|
4782
|
-
|
|
4783
|
-
|
|
5889
|
+
{{#unless hasPayment}}
|
|
5890
|
+
# Reuses one Agent per session_id so each session keeps its own in-process
|
|
5891
|
+
# conversation history (best-effort; resets on cold start). The cache is bounded
|
|
5892
|
+
# to 128 sessions with LRU eviction (least-recently-used is dropped and its
|
|
5893
|
+
# history reset) so a single process serving many sessions cannot leak history
|
|
5894
|
+
# between them or grow without limit. For durable history, attach a session manager.
|
|
5895
|
+
def agent_factory():
|
|
5896
|
+
cache = OrderedDict()
|
|
5897
|
+
def get_or_create_agent(session_id{{#if hasSkillsFetcher}}, skill_plugins=None{{/if}}):
|
|
5898
|
+
if session_id in cache:
|
|
5899
|
+
cache.move_to_end(session_id)
|
|
5900
|
+
return cache[session_id]
|
|
5901
|
+
if len(cache) >= 128:
|
|
5902
|
+
cache.popitem(last=False)
|
|
5903
|
+
cache[session_id] = Agent(
|
|
4784
5904
|
model=load_model(),
|
|
4785
|
-
system_prompt=
|
|
4786
|
-
tools=tools
|
|
5905
|
+
system_prompt=DEFAULT_SYSTEM_PROMPT,
|
|
5906
|
+
tools=tools,
|
|
5907
|
+
conversation_manager=_make_conversation_manager(),
|
|
5908
|
+
{{#if hasSkillsFetcher}}
|
|
5909
|
+
plugins=skill_plugins or None,
|
|
5910
|
+
{{/if}}
|
|
5911
|
+
{{#if hasExecutionLimits}}
|
|
5912
|
+
tool_executor=SequentialToolExecutor(),
|
|
5913
|
+
callback_handler=None,
|
|
5914
|
+
{{/if}}
|
|
5915
|
+
hooks=[
|
|
5916
|
+
{{#if hasExecutionLimits}}
|
|
5917
|
+
ExecutionLimitsHook(
|
|
5918
|
+
{{#if maxIterations}}max_iterations={{maxIterations}},{{/if}}
|
|
5919
|
+
{{#if maxTokens}}max_tokens={{maxTokens}},{{/if}}
|
|
5920
|
+
{{#if timeoutSeconds}}timeout_seconds={{timeoutSeconds}},{{/if}}
|
|
5921
|
+
),
|
|
5922
|
+
{{/if}}
|
|
5923
|
+
{{#if hasConfigBundle}}
|
|
5924
|
+
ConfigBundleHook(),
|
|
5925
|
+
{{/if}}
|
|
5926
|
+
],
|
|
4787
5927
|
)
|
|
4788
|
-
|
|
5928
|
+
return cache[session_id]
|
|
5929
|
+
return get_or_create_agent
|
|
5930
|
+
get_or_create_agent = agent_factory()
|
|
5931
|
+
{{/unless}}
|
|
4789
5932
|
{{/if}}
|
|
4790
5933
|
|
|
4791
5934
|
|
|
5935
|
+
def strip_trailing_tool_use(messages: Any) -> list[dict]:
|
|
5936
|
+
"""Strip toolUse blocks from the tail until the last message has none."""
|
|
5937
|
+
if not isinstance(messages, list):
|
|
5938
|
+
raise ValueError("messages must be a list")
|
|
5939
|
+
|
|
5940
|
+
messages = list(messages)
|
|
5941
|
+
while messages:
|
|
5942
|
+
last = messages[-1]
|
|
5943
|
+
if not isinstance(last, dict):
|
|
5944
|
+
raise ValueError("each message must be an object")
|
|
5945
|
+
original_content = last.get("content", [])
|
|
5946
|
+
if not isinstance(original_content, list) or not all(isinstance(block, dict) for block in original_content):
|
|
5947
|
+
raise ValueError("each message content value must be a list of content blocks")
|
|
5948
|
+
|
|
5949
|
+
content = [block for block in original_content if "toolUse" not in block]
|
|
5950
|
+
if len(content) == len(original_content):
|
|
5951
|
+
break
|
|
5952
|
+
if content:
|
|
5953
|
+
messages[-1] = {**last, "content": content}
|
|
5954
|
+
break
|
|
5955
|
+
messages.pop()
|
|
5956
|
+
|
|
5957
|
+
return messages
|
|
5958
|
+
|
|
5959
|
+
|
|
5960
|
+
def _extract_prompt(payload: dict):
|
|
5961
|
+
"""Accept validated harness messages, tool results, or a plain prompt string."""
|
|
5962
|
+
if not isinstance(payload, dict):
|
|
5963
|
+
raise ValueError("payload must be a JSON object")
|
|
5964
|
+
if "messages" in payload:
|
|
5965
|
+
return strip_trailing_tool_use(payload["messages"])
|
|
5966
|
+
if "tool_results" in payload:
|
|
5967
|
+
tool_results = payload["tool_results"]
|
|
5968
|
+
if not isinstance(tool_results, list) or not all(
|
|
5969
|
+
isinstance(tool_result, dict) and isinstance(tool_result.get("toolUseId"), str)
|
|
5970
|
+
for tool_result in tool_results
|
|
5971
|
+
):
|
|
5972
|
+
raise ValueError("tool_results must contain objects with a toolUseId string")
|
|
5973
|
+
return [{"role": "user", "content": [{"toolResult": {
|
|
5974
|
+
"toolUseId": tr["toolUseId"],
|
|
5975
|
+
"status": tr.get("status", "success"),
|
|
5976
|
+
"content": tr.get("content", []),
|
|
5977
|
+
}} for tr in tool_results]}]
|
|
5978
|
+
prompt = payload.get("prompt", "")
|
|
5979
|
+
if not isinstance(prompt, str):
|
|
5980
|
+
raise ValueError("prompt must be a string")
|
|
5981
|
+
return prompt
|
|
5982
|
+
|
|
5983
|
+
|
|
5984
|
+
def _has_inline_function_call(messages) -> bool:
|
|
5985
|
+
"""Return True if messages contains an assistant toolUse for an inline function tool."""
|
|
5986
|
+
if not _INLINE_FUNCTION_NAMES or not isinstance(messages, list):
|
|
5987
|
+
return False
|
|
5988
|
+
for msg in messages:
|
|
5989
|
+
if msg.get("role") == "assistant":
|
|
5990
|
+
for block in msg.get("content", []):
|
|
5991
|
+
if isinstance(block, dict) and block.get("toolUse", {}).get("name") in _INLINE_FUNCTION_NAMES:
|
|
5992
|
+
return True
|
|
5993
|
+
return False
|
|
5994
|
+
|
|
5995
|
+
|
|
5996
|
+
def _is_inline_function_call(event: dict) -> bool:
|
|
5997
|
+
"""Check if a contentBlockStart event is for an inline function tool."""
|
|
5998
|
+
if not _INLINE_FUNCTION_NAMES:
|
|
5999
|
+
return False
|
|
6000
|
+
cbs = event.get("contentBlockStart", {})
|
|
6001
|
+
start = cbs.get("start", {})
|
|
6002
|
+
tool_use = start.get("toolUse") if isinstance(start, dict) else None
|
|
6003
|
+
return tool_use is not None and tool_use.get("name") in _INLINE_FUNCTION_NAMES
|
|
6004
|
+
|
|
6005
|
+
|
|
6006
|
+
|
|
4792
6007
|
@app.entrypoint
|
|
4793
6008
|
async def invoke(payload, context):
|
|
4794
6009
|
log.info("Invoking Agent.....")
|
|
4795
6010
|
|
|
6011
|
+
{{#if hasPayment}}
|
|
6012
|
+
user_id = payload.get("user_id") or getattr(context, "user_id", "default-user")
|
|
6013
|
+
instrument_id = payload.get("payment_instrument_id")
|
|
6014
|
+
session_id = payload.get("payment_session_id")
|
|
6015
|
+
payments_plugin = create_payments_plugin(user_id, instrument_id, session_id)
|
|
6016
|
+
plugins = [payments_plugin] if payments_plugin else []
|
|
6017
|
+
{{/if}}
|
|
6018
|
+
{{#if hasSkillsFetcher}}
|
|
6019
|
+
skill_paths = [{{#each pathSkills}}{{safeJson this}}{{#unless @last}}, {{/unless}}{{/each}}]
|
|
6020
|
+
{{#if s3Skills}}
|
|
6021
|
+
s3_skill_sources = [{{#each s3Skills}}{{safeJson this}}{{#unless @last}}, {{/unless}}{{/each}}]
|
|
6022
|
+
skill_paths.extend(await asyncio.to_thread(resolve_s3_skills, s3_skill_sources, None))
|
|
6023
|
+
{{/if}}
|
|
6024
|
+
{{#if gitSkills}}
|
|
6025
|
+
git_skill_sources = [
|
|
6026
|
+
{{#each gitSkills}}
|
|
6027
|
+
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}}),
|
|
6028
|
+
{{/each}}
|
|
6029
|
+
]
|
|
6030
|
+
{{#if (some gitSkills "credentialArn")}}
|
|
6031
|
+
_git_identity_client = IdentityClient(os.environ.get("AWS_REGION", os.environ.get("AWS_DEFAULT_REGION", "us-east-1")))
|
|
6032
|
+
{{else}}
|
|
6033
|
+
_git_identity_client = None
|
|
6034
|
+
{{/if}}
|
|
6035
|
+
skill_paths.extend(await asyncio.to_thread(resolve_git_skills, git_skill_sources, _git_identity_client))
|
|
6036
|
+
{{/if}}
|
|
6037
|
+
_skill_plugins = [AgentSkills(skills=skill_paths)] if skill_paths else []
|
|
6038
|
+
{{/if}}
|
|
6039
|
+
|
|
4796
6040
|
{{#if hasMemory}}
|
|
6041
|
+
{{#if hasPayment}}
|
|
6042
|
+
mem_session_id = getattr(context, 'session_id', 'default-session')
|
|
6043
|
+
{{#if actorId}}
|
|
6044
|
+
mem_user_id = "{{actorId}}"
|
|
6045
|
+
{{else}}
|
|
6046
|
+
mem_user_id = getattr(context, 'user_id', 'default-user')
|
|
6047
|
+
{{/if}}
|
|
6048
|
+
agent = Agent(
|
|
6049
|
+
model=load_model(),
|
|
6050
|
+
session_manager=get_memory_session_manager(mem_session_id, mem_user_id),
|
|
6051
|
+
system_prompt=DEFAULT_SYSTEM_PROMPT + PAYMENT_SYSTEM_PROMPT,
|
|
6052
|
+
tools=tools,
|
|
6053
|
+
plugins=plugins{{#if hasSkillsFetcher}} + _skill_plugins{{/if}},{{#if hasConfigBundle}}
|
|
6054
|
+
hooks=[ConfigBundleHook()],{{/if}}
|
|
6055
|
+
)
|
|
6056
|
+
{{else}}
|
|
4797
6057
|
session_id = getattr(context, 'session_id', 'default-session')
|
|
6058
|
+
{{#if actorId}}
|
|
6059
|
+
user_id = "{{actorId}}"
|
|
6060
|
+
{{else}}
|
|
4798
6061
|
user_id = getattr(context, 'user_id', 'default-user')
|
|
4799
|
-
|
|
6062
|
+
{{/if}}
|
|
6063
|
+
agent = get_or_create_agent(session_id, user_id{{#if hasSkillsFetcher}}, _skill_plugins{{/if}})
|
|
6064
|
+
{{/if}}
|
|
6065
|
+
{{else}}
|
|
6066
|
+
{{#if hasPayment}}
|
|
6067
|
+
agent = Agent(
|
|
6068
|
+
model=load_model(),
|
|
6069
|
+
system_prompt=DEFAULT_SYSTEM_PROMPT + PAYMENT_SYSTEM_PROMPT,
|
|
6070
|
+
tools=tools,
|
|
6071
|
+
plugins=plugins{{#if hasSkillsFetcher}} + _skill_plugins{{/if}},{{#if hasConfigBundle}}
|
|
6072
|
+
hooks=[ConfigBundleHook()],{{/if}}
|
|
6073
|
+
)
|
|
4800
6074
|
{{else}}
|
|
4801
|
-
|
|
6075
|
+
session_id = getattr(context, 'session_id', 'default-session')
|
|
6076
|
+
agent = get_or_create_agent(session_id{{#if hasSkillsFetcher}}, _skill_plugins{{/if}})
|
|
6077
|
+
{{/if}}
|
|
4802
6078
|
{{/if}}
|
|
4803
6079
|
|
|
4804
|
-
|
|
4805
|
-
|
|
6080
|
+
prompt = _extract_prompt(payload)
|
|
6081
|
+
|
|
6082
|
+
{{#if inlineFunctionTools}}
|
|
6083
|
+
# If Turn 2 carries the harness-style assistant(toolUse)+user(toolResult) pair,
|
|
6084
|
+
# strip the placeholder turn Strands stored during Turn 1 so the real toolResult
|
|
6085
|
+
# is injected cleanly — same protocol as the harness runtime.
|
|
6086
|
+
if _has_inline_function_call(prompt):
|
|
6087
|
+
msgs = agent.messages
|
|
6088
|
+
if len(msgs) >= 2 and any("toolResult" in b for b in msgs[-1].get("content", [])):
|
|
6089
|
+
del msgs[-2:]
|
|
6090
|
+
{{/if}}
|
|
6091
|
+
|
|
6092
|
+
{{#if hasExecutionLimits}}
|
|
6093
|
+
timeout_seconds = {{#if timeoutSeconds}}{{timeoutSeconds}}{{else}}None{{/if}}
|
|
6094
|
+
timeout_fired = False
|
|
6095
|
+
watchdog_task = None
|
|
6096
|
+
if timeout_seconds is not None:
|
|
6097
|
+
async def _timeout_watchdog():
|
|
6098
|
+
nonlocal timeout_fired
|
|
6099
|
+
await asyncio.sleep(timeout_seconds)
|
|
6100
|
+
timeout_fired = True
|
|
6101
|
+
agent.cancel()
|
|
6102
|
+
watchdog_task = asyncio.create_task(_timeout_watchdog())
|
|
4806
6103
|
|
|
4807
|
-
|
|
4808
|
-
#
|
|
4809
|
-
|
|
4810
|
-
|
|
6104
|
+
try:
|
|
6105
|
+
{{#if inlineFunctionTools}}
|
|
6106
|
+
hit_inline_function = False
|
|
6107
|
+
{{/if}}
|
|
6108
|
+
async for event in agent.stream_async(
|
|
6109
|
+
prompt,
|
|
6110
|
+
):
|
|
6111
|
+
if not isinstance(event, dict) or "event" not in event:
|
|
6112
|
+
continue
|
|
6113
|
+
cbs = event["event"].get("contentBlockStart")
|
|
6114
|
+
if cbs is not None and not cbs.get("start"):
|
|
6115
|
+
continue
|
|
6116
|
+
{{#if inlineFunctionTools}}
|
|
6117
|
+
if not hit_inline_function:
|
|
6118
|
+
hit_inline_function = _is_inline_function_call(event["event"])
|
|
6119
|
+
{{/if}}
|
|
6120
|
+
yield event
|
|
6121
|
+
{{#if inlineFunctionTools}}
|
|
6122
|
+
if hit_inline_function and "messageStop" in event["event"]:
|
|
6123
|
+
return
|
|
6124
|
+
{{/if}}
|
|
6125
|
+
|
|
6126
|
+
if timeout_fired:
|
|
6127
|
+
yield {"event": {"messageStop": {"stopReason": "timeout_exceeded"}}}
|
|
6128
|
+
except EventLoopException as e:
|
|
6129
|
+
if isinstance(e.original_exception, ExecutionLimitExceeded):
|
|
6130
|
+
yield {"event": {"messageStop": {"stopReason": str(e.original_exception)}}}
|
|
6131
|
+
return
|
|
6132
|
+
raise
|
|
6133
|
+
finally:
|
|
6134
|
+
if watchdog_task is not None:
|
|
6135
|
+
watchdog_task.cancel()
|
|
6136
|
+
try:
|
|
6137
|
+
await watchdog_task
|
|
6138
|
+
except asyncio.CancelledError:
|
|
6139
|
+
pass
|
|
6140
|
+
{{else}}
|
|
6141
|
+
{{#if inlineFunctionTools}}
|
|
6142
|
+
hit_inline_function = False
|
|
6143
|
+
{{/if}}
|
|
6144
|
+
async for event in agent.stream_async(
|
|
6145
|
+
prompt,
|
|
6146
|
+
):
|
|
6147
|
+
if not isinstance(event, dict) or "event" not in event:
|
|
6148
|
+
continue
|
|
6149
|
+
cbs = event["event"].get("contentBlockStart")
|
|
6150
|
+
if cbs is not None and not cbs.get("start"):
|
|
6151
|
+
continue
|
|
6152
|
+
{{#if inlineFunctionTools}}
|
|
6153
|
+
if not hit_inline_function:
|
|
6154
|
+
hit_inline_function = _is_inline_function_call(event["event"])
|
|
6155
|
+
{{/if}}
|
|
6156
|
+
yield event
|
|
6157
|
+
{{#if inlineFunctionTools}}
|
|
6158
|
+
if hit_inline_function and "messageStop" in event["event"]:
|
|
6159
|
+
return
|
|
6160
|
+
{{/if}}
|
|
6161
|
+
{{/if}}
|
|
4811
6162
|
|
|
4812
6163
|
|
|
4813
6164
|
if __name__ == "__main__":
|
|
@@ -4841,7 +6192,10 @@ from bedrock_agentcore.identity import requires_access_token
|
|
|
4841
6192
|
@requires_access_token(
|
|
4842
6193
|
provider_name="{{credentialProviderName}}",
|
|
4843
6194
|
scopes=[{{#if scopes}}"{{scopes}}"{{/if}}],
|
|
4844
|
-
auth_flow="M2M",
|
|
6195
|
+
auth_flow="{{#if authFlow}}{{authFlow}}{{else}}M2M{{/if}}",
|
|
6196
|
+
{{#if customParameters}}
|
|
6197
|
+
custom_parameters={{safeJson customParameters}},
|
|
6198
|
+
{{/if}}
|
|
4845
6199
|
)
|
|
4846
6200
|
def _get_bearer_token_{{snakeCase name}}(*, access_token: str):
|
|
4847
6201
|
"""Obtain OAuth access token via AgentCore Identity for {{name}}."""
|
|
@@ -4852,18 +6206,22 @@ def _get_bearer_token_{{snakeCase name}}(*, access_token: str):
|
|
|
4852
6206
|
{{#each gatewayProviders}}
|
|
4853
6207
|
def get_{{snakeCase name}}_mcp_client() -> MCPClient | None:
|
|
4854
6208
|
"""Returns an MCP Client connected to the {{name}} gateway."""
|
|
6209
|
+
{{#if hardcodedUrl}}
|
|
6210
|
+
url = {{safeJson hardcodedUrl}}
|
|
6211
|
+
{{else}}
|
|
4855
6212
|
url = os.environ.get("{{envVarName}}")
|
|
4856
6213
|
if not url:
|
|
4857
6214
|
logger.warning("{{envVarName}} not set — {{name}} gateway tools unavailable")
|
|
4858
6215
|
return None
|
|
6216
|
+
{{/if}}
|
|
4859
6217
|
{{#if (eq authType "AWS_IAM")}}
|
|
4860
|
-
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"))))
|
|
6218
|
+
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}}")
|
|
4861
6219
|
{{else if (eq authType "CUSTOM_JWT")}}
|
|
4862
6220
|
token = _get_bearer_token_{{snakeCase name}}()
|
|
4863
6221
|
headers = {"Authorization": f"Bearer {token}"} if token else {}
|
|
4864
|
-
return MCPClient(lambda: streamablehttp_client(url, headers=headers))
|
|
6222
|
+
return MCPClient(lambda: streamablehttp_client(url, headers=headers), prefix="{{snakeCase name}}")
|
|
4865
6223
|
{{else}}
|
|
4866
|
-
return MCPClient(lambda: streamablehttp_client(url))
|
|
6224
|
+
return MCPClient(lambda: streamablehttp_client(url), prefix="{{snakeCase name}}")
|
|
4867
6225
|
{{/if}}
|
|
4868
6226
|
|
|
4869
6227
|
{{/each}}
|
|
@@ -4876,7 +6234,41 @@ def get_all_gateway_mcp_clients() -> list[MCPClient]:
|
|
|
4876
6234
|
clients.append(client)
|
|
4877
6235
|
{{/each}}
|
|
4878
6236
|
return clients
|
|
4879
|
-
{{
|
|
6237
|
+
{{/if}}
|
|
6238
|
+
{{#if remoteMcpTools}}
|
|
6239
|
+
{{#if (some remoteMcpTools "headerCredentials")}}
|
|
6240
|
+
from bedrock_agentcore.identity.auth import requires_api_key
|
|
6241
|
+
{{/if}}
|
|
6242
|
+
{{#each remoteMcpTools}}
|
|
6243
|
+
{{#if headerCredentials}}
|
|
6244
|
+
{{#each headerCredentials}}
|
|
6245
|
+
@requires_api_key(provider_name="{{credentialName}}")
|
|
6246
|
+
def _get_{{snakeCase ../name}}_{{snakeCase headerKey}}_key(api_key: str) -> str:
|
|
6247
|
+
"""Fetch {{headerKey}} credential for {{../name}} from AgentCore Identity."""
|
|
6248
|
+
return api_key
|
|
6249
|
+
|
|
6250
|
+
{{/each}}
|
|
6251
|
+
{{/if}}
|
|
6252
|
+
def get_{{snakeCase name}}_mcp_client() -> MCPClient | None:
|
|
6253
|
+
"""Returns an MCP Client for the {{name}} remote MCP server."""
|
|
6254
|
+
url = {{safeJson url}}
|
|
6255
|
+
{{#if headerCredentials}}
|
|
6256
|
+
if os.getenv("LOCAL_DEV") == "1":
|
|
6257
|
+
headers = { {{#each headerCredentials}}{{safeJson headerKey}}: os.environ.get("{{envVarName}}", ""){{#unless @last}}, {{/unless}}{{/each}} }
|
|
6258
|
+
else:
|
|
6259
|
+
headers = { {{#each headerCredentials}}{{safeJson headerKey}}: _get_{{snakeCase ../name}}_{{snakeCase headerKey}}_key(){{#unless @last}}, {{/unless}}{{/each}} }
|
|
6260
|
+
return MCPClient(lambda: streamablehttp_client(url, headers=headers))
|
|
6261
|
+
{{else}}
|
|
6262
|
+
return MCPClient(lambda: streamablehttp_client(url))
|
|
6263
|
+
{{/if}}
|
|
6264
|
+
|
|
6265
|
+
{{/each}}
|
|
6266
|
+
def get_all_remote_mcp_clients() -> list[MCPClient]:
|
|
6267
|
+
"""Returns all configured remote MCP clients."""
|
|
6268
|
+
clients = [{{#each remoteMcpTools}}get_{{snakeCase name}}_mcp_client(){{#unless @last}}, {{/unless}}{{/each}}]
|
|
6269
|
+
return [c for c in clients if c is not None]
|
|
6270
|
+
{{/if}}
|
|
6271
|
+
{{#unless (or hasGateway remoteMcpTools)}}
|
|
4880
6272
|
{{#if isVpc}}
|
|
4881
6273
|
# VPC mode: external MCP endpoints are not reachable without a NAT gateway.
|
|
4882
6274
|
# Add an AgentCore Gateway with \`agentcore add gateway\`, or configure your own endpoint below.
|
|
@@ -4885,6 +6277,7 @@ def get_streamable_http_mcp_client() -> MCPClient | None:
|
|
|
4885
6277
|
"""No MCP server configured. Add a gateway with \`agentcore add gateway\`."""
|
|
4886
6278
|
return None
|
|
4887
6279
|
{{else}}
|
|
6280
|
+
{{#unless isExportHarness}}
|
|
4888
6281
|
# ExaAI provides information about code through web searches, crawling and code context searches through their platform. Requires no authentication
|
|
4889
6282
|
EXAMPLE_MCP_ENDPOINT = "https://mcp.exa.ai/mcp"
|
|
4890
6283
|
|
|
@@ -4892,8 +6285,9 @@ def get_streamable_http_mcp_client() -> MCPClient:
|
|
|
4892
6285
|
"""Returns an MCP Client compatible with Strands"""
|
|
4893
6286
|
# to use an MCP server that supports bearer authentication, add headers={"Authorization": f"Bearer {access_token}"}
|
|
4894
6287
|
return MCPClient(lambda: streamablehttp_client(EXAMPLE_MCP_ENDPOINT))
|
|
6288
|
+
{{/unless}}
|
|
4895
6289
|
{{/if}}
|
|
4896
|
-
{{/
|
|
6290
|
+
{{/unless}}
|
|
4897
6291
|
"
|
|
4898
6292
|
`;
|
|
4899
6293
|
|
|
@@ -4904,12 +6298,74 @@ exports[`Assets Directory Snapshots > Python framework assets > python/python/ht
|
|
|
4904
6298
|
|
|
4905
6299
|
exports[`Assets Directory Snapshots > Python framework assets > python/python/http/strands/base/model/load.py should match snapshot 1`] = `
|
|
4906
6300
|
"{{#if (eq modelProvider "Bedrock")}}
|
|
6301
|
+
{{#if bedrockMantle}}
|
|
6302
|
+
import os
|
|
6303
|
+
|
|
6304
|
+
from aws_bedrock_token_generator import provide_token
|
|
6305
|
+
{{#if (eq mantleApiFormat "chat_completions")}}
|
|
6306
|
+
from strands.models.openai import OpenAIModel
|
|
6307
|
+
{{else}}
|
|
6308
|
+
{{#if mantleProprietary}}
|
|
6309
|
+
from strands.models.openai_responses import OpenAIResponsesModel
|
|
6310
|
+
{{else}}
|
|
6311
|
+
from model.mantle_compat import MantleCompatResponsesModel
|
|
6312
|
+
{{/if}}
|
|
6313
|
+
{{/if}}
|
|
6314
|
+
|
|
6315
|
+
MODEL_ID = "{{modelId}}"
|
|
6316
|
+
|
|
6317
|
+
|
|
6318
|
+
def load_model():
|
|
6319
|
+
"""
|
|
6320
|
+
Get a Bedrock Mantle model client. These OpenAI-compatible models (e.g. openai.gpt-5.5,
|
|
6321
|
+
openai.gpt-oss-120b) are served via the Bedrock Mantle endpoint, NOT the Converse API — so they
|
|
6322
|
+
are invoked through an OpenAI-style client authenticated with a short-lived Bedrock bearer token.
|
|
6323
|
+
Region is read from AWS_REGION (set by the AgentCore runtime).
|
|
6324
|
+
"""
|
|
6325
|
+
region = os.environ.get("AWS_REGION", os.environ.get("AWS_DEFAULT_REGION", "us-east-1"))
|
|
6326
|
+
token = provide_token(region=region)
|
|
6327
|
+
{{#if mantleProprietary}}
|
|
6328
|
+
# Proprietary OpenAI models only work on the /openai/v1 Mantle path.
|
|
6329
|
+
base_url = f"https://bedrock-mantle.{region}.api.aws/openai/v1"
|
|
6330
|
+
{{else}}
|
|
6331
|
+
# Open-source OpenAI models (gpt-oss-*) only work on the /v1 Mantle path.
|
|
6332
|
+
base_url = f"https://bedrock-mantle.{region}.api.aws/v1"
|
|
6333
|
+
{{/if}}
|
|
6334
|
+
client_args = {"api_key": token, "base_url": base_url}
|
|
6335
|
+
|
|
6336
|
+
params = {}
|
|
6337
|
+
{{#if modelMaxTokens}}
|
|
6338
|
+
{{#if (eq mantleApiFormat "chat_completions")}}
|
|
6339
|
+
params["max_completion_tokens"] = {{modelMaxTokens}}
|
|
6340
|
+
{{else}}
|
|
6341
|
+
params["max_output_tokens"] = {{modelMaxTokens}}
|
|
6342
|
+
{{/if}}
|
|
6343
|
+
{{/if}}
|
|
6344
|
+
{{#if modelTemperature}}
|
|
6345
|
+
params["temperature"] = {{modelTemperature}}
|
|
6346
|
+
{{/if}}
|
|
6347
|
+
{{#if modelTopP}}
|
|
6348
|
+
params["top_p"] = {{modelTopP}}
|
|
6349
|
+
{{/if}}
|
|
6350
|
+
{{#if (eq mantleApiFormat "chat_completions")}}
|
|
6351
|
+
return OpenAIModel(client_args=client_args, model_id=MODEL_ID, params=params)
|
|
6352
|
+
{{else}}
|
|
6353
|
+
# Responses API: Mantle does not persist responses, so disable server-side storage.
|
|
6354
|
+
params["store"] = False
|
|
6355
|
+
{{#if mantleProprietary}}
|
|
6356
|
+
return OpenAIResponsesModel(client_args=client_args, model_id=MODEL_ID, params=params)
|
|
6357
|
+
{{else}}
|
|
6358
|
+
return MantleCompatResponsesModel(client_args=client_args, model_id=MODEL_ID, params=params)
|
|
6359
|
+
{{/if}}
|
|
6360
|
+
{{/if}}
|
|
6361
|
+
{{else}}
|
|
4907
6362
|
from strands.models.bedrock import BedrockModel
|
|
4908
6363
|
|
|
4909
6364
|
|
|
4910
6365
|
def load_model() -> BedrockModel:
|
|
4911
6366
|
"""Get Bedrock model client using IAM credentials."""
|
|
4912
|
-
return BedrockModel(model_id="global.anthropic.claude-sonnet-4-5-20250929-v1:0")
|
|
6367
|
+
return BedrockModel(model_id="{{#if modelId}}{{modelId}}{{else}}global.anthropic.claude-sonnet-4-5-20250929-v1:0{{/if}}"{{#if modelMaxTokens}}, max_tokens={{modelMaxTokens}}{{/if}})
|
|
6368
|
+
{{/if}}
|
|
4913
6369
|
{{/if}}
|
|
4914
6370
|
{{#if (eq modelProvider "Anthropic")}}
|
|
4915
6371
|
import os
|
|
@@ -4985,7 +6441,7 @@ def load_model() -> OpenAIModel:
|
|
|
4985
6441
|
"""Get authenticated OpenAI model client."""
|
|
4986
6442
|
return OpenAIModel(
|
|
4987
6443
|
client_args={"api_key": _get_api_key()},
|
|
4988
|
-
model_id="gpt-4.1",
|
|
6444
|
+
model_id="{{#if modelId}}{{modelId}}{{else}}gpt-4.1{{/if}}",
|
|
4989
6445
|
)
|
|
4990
6446
|
{{/if}}
|
|
4991
6447
|
{{#if (eq modelProvider "Gemini")}}
|
|
@@ -5023,10 +6479,89 @@ def load_model() -> GeminiModel:
|
|
|
5023
6479
|
"""Get authenticated Gemini model client."""
|
|
5024
6480
|
return GeminiModel(
|
|
5025
6481
|
client_args={"api_key": _get_api_key()},
|
|
5026
|
-
model_id="gemini-2.5-flash",
|
|
6482
|
+
model_id="{{#if modelId}}{{modelId}}{{else}}gemini-2.5-flash{{/if}}",
|
|
5027
6483
|
)
|
|
5028
6484
|
{{/if}}
|
|
5029
|
-
"
|
|
6485
|
+
{{#if (eq modelProvider "LiteLLM")}}
|
|
6486
|
+
import os
|
|
6487
|
+
{{#if litellmAdditionalParams}}
|
|
6488
|
+
import json
|
|
6489
|
+
{{/if}}
|
|
6490
|
+
|
|
6491
|
+
from strands.models.litellm import LiteLLMModel
|
|
6492
|
+
{{#if identityProviders.[0].name}}
|
|
6493
|
+
from bedrock_agentcore.identity.auth import requires_api_key
|
|
6494
|
+
|
|
6495
|
+
IDENTITY_PROVIDER_NAME = "{{identityProviders.[0].name}}"
|
|
6496
|
+
IDENTITY_ENV_VAR = "{{identityProviders.[0].envVarName}}"
|
|
6497
|
+
|
|
6498
|
+
|
|
6499
|
+
@requires_api_key(provider_name=IDENTITY_PROVIDER_NAME)
|
|
6500
|
+
def _agentcore_identity_api_key_provider(api_key: str) -> str:
|
|
6501
|
+
"""Fetch API key from AgentCore Identity."""
|
|
6502
|
+
return api_key
|
|
6503
|
+
|
|
6504
|
+
|
|
6505
|
+
def _get_api_key() -> str:
|
|
6506
|
+
"""
|
|
6507
|
+
Uses AgentCore Identity for API key management in deployed environments.
|
|
6508
|
+
For local development, run via 'agentcore dev' which loads agentcore/.env.
|
|
6509
|
+
"""
|
|
6510
|
+
if os.getenv("LOCAL_DEV") == "1":
|
|
6511
|
+
api_key = os.getenv(IDENTITY_ENV_VAR)
|
|
6512
|
+
if not api_key:
|
|
6513
|
+
raise RuntimeError(
|
|
6514
|
+
f"{IDENTITY_ENV_VAR} not found. Add {IDENTITY_ENV_VAR}=your-key to .env.local"
|
|
6515
|
+
)
|
|
6516
|
+
return api_key
|
|
6517
|
+
return _agentcore_identity_api_key_provider()
|
|
6518
|
+
{{/if}}
|
|
6519
|
+
|
|
6520
|
+
|
|
6521
|
+
|
|
6522
|
+
|
|
6523
|
+
def load_model() -> LiteLLMModel:
|
|
6524
|
+
"""Get a LiteLLM model client (proxies to the provider encoded in model_id)."""
|
|
6525
|
+
client_args = {}
|
|
6526
|
+
{{#if identityProviders.[0].name}}
|
|
6527
|
+
client_args["api_key"] = _get_api_key()
|
|
6528
|
+
{{/if}}
|
|
6529
|
+
{{#if litellmApiBase}}
|
|
6530
|
+
client_args["api_base"] = {{safeJson litellmApiBase}}
|
|
6531
|
+
{{/if}}
|
|
6532
|
+
params = {{#if litellmAdditionalParams}}json.loads({{pyJsonStr litellmAdditionalParams}}){{else}}{}{{/if}}
|
|
6533
|
+
return LiteLLMModel(
|
|
6534
|
+
client_args=client_args,
|
|
6535
|
+
model_id="{{#if modelId}}{{modelId}}{{else}}bedrock/us.anthropic.claude-sonnet-4-5-20250514-v1:0{{/if}}",
|
|
6536
|
+
params=params,
|
|
6537
|
+
)
|
|
6538
|
+
{{/if}}
|
|
6539
|
+
"
|
|
6540
|
+
`;
|
|
6541
|
+
|
|
6542
|
+
exports[`Assets Directory Snapshots > Python framework assets > python/python/http/strands/base/model/mantle_compat.py should match snapshot 1`] = `
|
|
6543
|
+
"from strands.models.openai_responses import OpenAIResponsesModel
|
|
6544
|
+
|
|
6545
|
+
|
|
6546
|
+
class MantleCompatResponsesModel(OpenAIResponsesModel):
|
|
6547
|
+
"""Workaround for Bedrock Mantle rejecting output_text in EasyInputMessage content arrays.
|
|
6548
|
+
|
|
6549
|
+
Mantle's Pydantic validation only accepts content as a plain string for assistant messages, while
|
|
6550
|
+
real OpenAI accepts both formats. Flatten assistant content arrays to strings so multi-turn works.
|
|
6551
|
+
Used for open-source OpenAI models (gpt-oss-*) on the /v1 Mantle path; proprietary models use the
|
|
6552
|
+
plain OpenAIResponsesModel on /openai/v1.
|
|
6553
|
+
"""
|
|
6554
|
+
|
|
6555
|
+
@classmethod
|
|
6556
|
+
def _format_request_messages(cls, messages):
|
|
6557
|
+
formatted = super()._format_request_messages(messages)
|
|
6558
|
+
for msg in formatted:
|
|
6559
|
+
if msg.get("role") == "assistant" and isinstance(msg.get("content"), list):
|
|
6560
|
+
msg["content"] = "".join(
|
|
6561
|
+
part.get("text", "") for part in msg["content"] if part.get("type") == "output_text"
|
|
6562
|
+
)
|
|
6563
|
+
return formatted
|
|
6564
|
+
"
|
|
5030
6565
|
`;
|
|
5031
6566
|
|
|
5032
6567
|
exports[`Assets Directory Snapshots > Python framework assets > python/python/http/strands/base/pyproject.toml should match snapshot 1`] = `
|
|
@@ -5042,14 +6577,20 @@ readme = "README.md"
|
|
|
5042
6577
|
requires-python = ">=3.10"
|
|
5043
6578
|
dependencies = [
|
|
5044
6579
|
{{#if (eq modelProvider "Anthropic")}}"anthropic >= 0.30.0",
|
|
5045
|
-
{{/if}}"aws-opentelemetry-distro",
|
|
5046
|
-
"bedrock-agentcore >= 1.
|
|
6580
|
+
{{/if}}"aws-opentelemetry-distro ~= 0.18.0",
|
|
6581
|
+
"bedrock-agentcore >= 1.9.1",
|
|
5047
6582
|
"botocore[crt] >= 1.35.0",
|
|
5048
6583
|
{{#if (eq modelProvider "Gemini")}}"google-genai >= 1.0.0",
|
|
5049
|
-
{{/if}}"mcp
|
|
6584
|
+
{{/if}}"mcp ~= 1.24.0",
|
|
5050
6585
|
{{#if (eq modelProvider "OpenAI")}}"openai >= 1.0.0",
|
|
5051
|
-
{{/if}}"
|
|
5052
|
-
{{
|
|
6586
|
+
{{/if}}{{#if (eq modelProvider "LiteLLM")}}"litellm >= 1.0.0",
|
|
6587
|
+
{{/if}}{{#if bedrockMantle}}"openai >= 1.0.0",
|
|
6588
|
+
"aws-bedrock-token-generator >= 1.0.0",
|
|
6589
|
+
{{/if}}"strands-agents >= 1.15.0",
|
|
6590
|
+
{{#if (or hasBrowser hasCodeInterpreter)}}"strands-agents-tools >= 0.1.0",
|
|
6591
|
+
{{/if}}{{#if hasBrowser}}"nest-asyncio >= 1.5.0",
|
|
6592
|
+
"playwright >= 1.42.0",
|
|
6593
|
+
{{/if}}{{#if hasGateway}}{{#if (includes gatewayAuthTypes "AWS_IAM")}}"mcp-proxy-for-aws >= 1.1.0",
|
|
5053
6594
|
{{/if}}{{/if}}
|
|
5054
6595
|
]
|
|
5055
6596
|
|
|
@@ -5058,6 +6599,347 @@ packages = ["."]
|
|
|
5058
6599
|
"
|
|
5059
6600
|
`;
|
|
5060
6601
|
|
|
6602
|
+
exports[`Assets Directory Snapshots > Python framework assets > python/python/http/strands/base/skills/fetcher.py should match snapshot 1`] = `
|
|
6603
|
+
""""Skill fetcher — downloads s3/git skills to local filesystem on first use.
|
|
6604
|
+
|
|
6605
|
+
Resolved paths are passed to AgentSkills(skills=...) in main.py.
|
|
6606
|
+
Cache directory: <tmpdir>/.agents/skills/ — an absolute path under the system temp
|
|
6607
|
+
directory (honors $TMPDIR, defaults to /tmp). The runtime working directory (e.g.
|
|
6608
|
+
/var/task in a CodeZip runtime) is read-only, so the cache must live somewhere
|
|
6609
|
+
guaranteed-writable.
|
|
6610
|
+
"""
|
|
6611
|
+
|
|
6612
|
+
import base64
|
|
6613
|
+
import hashlib
|
|
6614
|
+
import json
|
|
6615
|
+
import logging
|
|
6616
|
+
import os
|
|
6617
|
+
import shutil
|
|
6618
|
+
import subprocess
|
|
6619
|
+
import tempfile
|
|
6620
|
+
from pathlib import Path
|
|
6621
|
+
from typing import Optional
|
|
6622
|
+
|
|
6623
|
+
logger = logging.getLogger(__name__)
|
|
6624
|
+
|
|
6625
|
+
_SKILLS_BASE = Path(tempfile.gettempdir()) / ".agents" / "skills"
|
|
6626
|
+
_GIT_TIMEOUT = 60
|
|
6627
|
+
_S3_MAX_SIZE_BYTES = 1 * 1024 * 1024 * 1024 # 1 GB
|
|
6628
|
+
|
|
6629
|
+
|
|
6630
|
+
def _stable_hash(value: str) -> str:
|
|
6631
|
+
return hashlib.sha256(value.encode()).hexdigest()[:12]
|
|
6632
|
+
|
|
6633
|
+
|
|
6634
|
+
def _cleanup(path: Path) -> None:
|
|
6635
|
+
"""Remove a partially-created skill directory so retries don't see stale state."""
|
|
6636
|
+
shutil.rmtree(path, ignore_errors=True)
|
|
6637
|
+
|
|
6638
|
+
|
|
6639
|
+
def _read_map(type_dir: Path) -> dict:
|
|
6640
|
+
map_file = type_dir / ".map.json"
|
|
6641
|
+
return json.loads(map_file.read_text()) if map_file.exists() else {}
|
|
6642
|
+
|
|
6643
|
+
|
|
6644
|
+
def _write_map(type_dir: Path, mapping: dict) -> None:
|
|
6645
|
+
type_dir.mkdir(parents=True, exist_ok=True)
|
|
6646
|
+
(type_dir / ".map.json").write_text(json.dumps(mapping))
|
|
6647
|
+
|
|
6648
|
+
|
|
6649
|
+
def _resolve_cached(type_dir: Path, source_hash: str) -> Optional[str]:
|
|
6650
|
+
"""Return the cached skill directory for a source hash, or None if not on disk."""
|
|
6651
|
+
mapping = _read_map(type_dir)
|
|
6652
|
+
dir_name = mapping.get(source_hash)
|
|
6653
|
+
if dir_name and (type_dir / dir_name).exists():
|
|
6654
|
+
return str(type_dir / dir_name)
|
|
6655
|
+
return None
|
|
6656
|
+
|
|
6657
|
+
|
|
6658
|
+
def _read_skill_name(skill_dir: Path) -> str:
|
|
6659
|
+
"""Extract the skill name from SKILL.md YAML frontmatter."""
|
|
6660
|
+
content = (skill_dir / "SKILL.md").read_text()
|
|
6661
|
+
if not content.startswith("---"):
|
|
6662
|
+
raise ValueError(f"SKILL.md in {skill_dir} has no YAML frontmatter (must start with ---)")
|
|
6663
|
+
parts = content.split("---", 2)
|
|
6664
|
+
if len(parts) < 3:
|
|
6665
|
+
raise ValueError(f"SKILL.md in {skill_dir} has malformed frontmatter (missing closing ---)")
|
|
6666
|
+
for line in parts[1].strip().splitlines():
|
|
6667
|
+
if line.startswith("name:"):
|
|
6668
|
+
name = line[len("name:"):].strip().strip("\\"'")
|
|
6669
|
+
if name:
|
|
6670
|
+
return name
|
|
6671
|
+
raise ValueError(f"SKILL.md in {skill_dir} is missing a 'name' field in frontmatter")
|
|
6672
|
+
|
|
6673
|
+
|
|
6674
|
+
def _pick_dir_name(type_dir: Path, name: str, source_hash: str) -> str:
|
|
6675
|
+
"""Pick a unique directory name, appending a hash suffix on collision."""
|
|
6676
|
+
if not (type_dir / name).exists():
|
|
6677
|
+
return name
|
|
6678
|
+
return f"{name}-{source_hash[:8]}"
|
|
6679
|
+
|
|
6680
|
+
|
|
6681
|
+
def _rename_and_cache_skill(type_dir: Path, temp_dir: Path, source_hash: str, skill_root: Path,
|
|
6682
|
+
source_label: str = "") -> Path:
|
|
6683
|
+
"""Validate SKILL.md, rename the temp dir to the skill's declared name, and update the map.
|
|
6684
|
+
|
|
6685
|
+
Raises ValueError if SKILL.md is missing or has invalid frontmatter.
|
|
6686
|
+
"""
|
|
6687
|
+
if not (skill_root / "SKILL.md").exists():
|
|
6688
|
+
_cleanup(temp_dir)
|
|
6689
|
+
hint = f" (source: {source_label})" if source_label else ""
|
|
6690
|
+
raise ValueError(f"No SKILL.md found in fetched skill{hint}")
|
|
6691
|
+
|
|
6692
|
+
name = _read_skill_name(skill_root)
|
|
6693
|
+
dir_name = _pick_dir_name(type_dir, name, source_hash)
|
|
6694
|
+
final_dir = type_dir / dir_name
|
|
6695
|
+
if final_dir != temp_dir:
|
|
6696
|
+
temp_dir.rename(final_dir)
|
|
6697
|
+
|
|
6698
|
+
mapping = _read_map(type_dir)
|
|
6699
|
+
mapping[source_hash] = dir_name
|
|
6700
|
+
_write_map(type_dir, mapping)
|
|
6701
|
+
return final_dir
|
|
6702
|
+
|
|
6703
|
+
|
|
6704
|
+
def _fetch_s3_skill(source: str, s3_client=None) -> Path:
|
|
6705
|
+
"""Download an s3:// skill prefix and return the local directory."""
|
|
6706
|
+
uri = source if source.endswith("/") else source + "/"
|
|
6707
|
+
source_hash = _stable_hash(uri)
|
|
6708
|
+
type_dir = _SKILLS_BASE / "s3"
|
|
6709
|
+
|
|
6710
|
+
cached = _resolve_cached(type_dir, source_hash)
|
|
6711
|
+
if cached:
|
|
6712
|
+
return Path(cached)
|
|
6713
|
+
|
|
6714
|
+
import boto3
|
|
6715
|
+
client = s3_client or boto3.client("s3")
|
|
6716
|
+
bucket, _, prefix = uri[len("s3://"):].partition("/")
|
|
6717
|
+
if not bucket:
|
|
6718
|
+
raise ValueError(f"Invalid S3 URI (no bucket): {uri}")
|
|
6719
|
+
|
|
6720
|
+
temp_dir = type_dir / source_hash
|
|
6721
|
+
_cleanup(temp_dir)
|
|
6722
|
+
temp_dir.mkdir(parents=True, exist_ok=True)
|
|
6723
|
+
temp_root = temp_dir.resolve()
|
|
6724
|
+
|
|
6725
|
+
paginator = client.get_paginator("list_objects_v2")
|
|
6726
|
+
total = 0
|
|
6727
|
+
for page in paginator.paginate(Bucket=bucket, Prefix=prefix):
|
|
6728
|
+
for obj in page.get("Contents", []):
|
|
6729
|
+
total += obj["Size"]
|
|
6730
|
+
if total > _S3_MAX_SIZE_BYTES:
|
|
6731
|
+
_cleanup(temp_dir)
|
|
6732
|
+
raise ValueError(f"S3 skill {uri} exceeds 1 GB size limit")
|
|
6733
|
+
rel = obj["Key"][len(prefix):].lstrip("/")
|
|
6734
|
+
if not rel:
|
|
6735
|
+
continue
|
|
6736
|
+
dest = (temp_dir / rel).resolve()
|
|
6737
|
+
if dest != temp_root and not str(dest).startswith(str(temp_root) + os.sep):
|
|
6738
|
+
_cleanup(temp_dir)
|
|
6739
|
+
raise ValueError(f"Path traversal detected in S3 key: {obj['Key']}")
|
|
6740
|
+
dest.parent.mkdir(parents=True, exist_ok=True)
|
|
6741
|
+
client.download_file(bucket, obj["Key"], str(dest))
|
|
6742
|
+
|
|
6743
|
+
if total == 0:
|
|
6744
|
+
_cleanup(temp_dir)
|
|
6745
|
+
raise ValueError(f"No files found at S3 URI: {uri}")
|
|
6746
|
+
|
|
6747
|
+
return _rename_and_cache_skill(type_dir, temp_dir, source_hash, temp_dir, source_label=uri)
|
|
6748
|
+
|
|
6749
|
+
|
|
6750
|
+
def _resolve_credential_arn(credential_arn: str, identity_client) -> str:
|
|
6751
|
+
"""Resolve a Token Vault API-key credential ARN to its secret value via AgentCore Identity.
|
|
6752
|
+
|
|
6753
|
+
ARN format: arn:<p>:bedrock-agentcore:<region>:<account>:token-vault/<vault>/apikeycredentialprovider/<name>
|
|
6754
|
+
"""
|
|
6755
|
+
from bedrock_agentcore.runtime.context import BedrockAgentCoreContext # noqa: PLC0415
|
|
6756
|
+
|
|
6757
|
+
provider_name = credential_arn.rsplit("/", 1)[-1]
|
|
6758
|
+
if not provider_name:
|
|
6759
|
+
raise ValueError(f"Invalid credential ARN: {credential_arn}")
|
|
6760
|
+
workload_token = BedrockAgentCoreContext.get_workload_access_token()
|
|
6761
|
+
if not workload_token:
|
|
6762
|
+
raise ValueError("Credential ARN resolution requires a workload access token")
|
|
6763
|
+
api_key = identity_client.dp_client.get_resource_api_key(
|
|
6764
|
+
resourceCredentialProviderName=provider_name,
|
|
6765
|
+
workloadIdentityToken=workload_token,
|
|
6766
|
+
)["apiKey"]
|
|
6767
|
+
if not api_key:
|
|
6768
|
+
raise ValueError(f"Identity returned empty API key for provider: {provider_name}")
|
|
6769
|
+
return api_key
|
|
6770
|
+
|
|
6771
|
+
|
|
6772
|
+
def _build_git_auth_env(credential_arn: Optional[str], username: Optional[str], identity_client=None) -> dict:
|
|
6773
|
+
"""Build GIT_CONFIG_* env vars for HTTP Basic auth using a Token Vault credential ARN.
|
|
6774
|
+
|
|
6775
|
+
Uses env vars instead of -c args to avoid leaking credentials in /proc/*/cmdline,
|
|
6776
|
+
and so auth propagates to sub-commands (e.g. sparse-checkout triggering a fetch).
|
|
6777
|
+
"""
|
|
6778
|
+
if not credential_arn or not identity_client:
|
|
6779
|
+
return {}
|
|
6780
|
+
password = _resolve_credential_arn(credential_arn, identity_client)
|
|
6781
|
+
user = username or "oauth2"
|
|
6782
|
+
encoded = base64.b64encode(f"{user}:{password}".encode()).decode()
|
|
6783
|
+
return {
|
|
6784
|
+
"GIT_CONFIG_COUNT": "1",
|
|
6785
|
+
"GIT_CONFIG_KEY_0": "http.extraHeader",
|
|
6786
|
+
"GIT_CONFIG_VALUE_0": f"Authorization: Basic {encoded}",
|
|
6787
|
+
}
|
|
6788
|
+
|
|
6789
|
+
|
|
6790
|
+
def _fetch_git_skill(url: str, skill_path: str = "", credential_arn: Optional[str] = None,
|
|
6791
|
+
username: Optional[str] = None, identity_client=None) -> Path:
|
|
6792
|
+
"""Shallow-clone a git skill repository and return the local skill directory.
|
|
6793
|
+
|
|
6794
|
+
Returns the directory containing SKILL.md (the subdir itself for sparse checkouts).
|
|
6795
|
+
"""
|
|
6796
|
+
if skill_path and (os.path.isabs(skill_path) or ".." in Path(skill_path).parts):
|
|
6797
|
+
raise ValueError(f"Path traversal detected in skill path: {skill_path}")
|
|
6798
|
+
|
|
6799
|
+
source_hash = _stable_hash(f"{url}:{skill_path}")
|
|
6800
|
+
type_dir = _SKILLS_BASE / "git"
|
|
6801
|
+
|
|
6802
|
+
cached = _resolve_cached(type_dir, source_hash)
|
|
6803
|
+
if cached:
|
|
6804
|
+
return Path(cached) / skill_path if skill_path else Path(cached)
|
|
6805
|
+
|
|
6806
|
+
temp_dir = type_dir / source_hash
|
|
6807
|
+
_cleanup(temp_dir)
|
|
6808
|
+
temp_dir.mkdir(parents=True, exist_ok=True)
|
|
6809
|
+
|
|
6810
|
+
extra_env = _build_git_auth_env(credential_arn, username, identity_client)
|
|
6811
|
+
git_env = {**os.environ, **extra_env} if extra_env else None
|
|
6812
|
+
|
|
6813
|
+
try:
|
|
6814
|
+
if skill_path:
|
|
6815
|
+
subprocess.run(
|
|
6816
|
+
["git", "clone", "--depth", "1", "--filter=blob:none", "--sparse", url, str(temp_dir)],
|
|
6817
|
+
check=True, timeout=_GIT_TIMEOUT, capture_output=True, env=git_env,
|
|
6818
|
+
)
|
|
6819
|
+
subprocess.run(
|
|
6820
|
+
["git", "sparse-checkout", "set", skill_path],
|
|
6821
|
+
check=True, timeout=_GIT_TIMEOUT, capture_output=True, cwd=str(temp_dir), env=git_env,
|
|
6822
|
+
)
|
|
6823
|
+
else:
|
|
6824
|
+
subprocess.run(
|
|
6825
|
+
["git", "clone", "--depth", "1", url, str(temp_dir)],
|
|
6826
|
+
check=True, timeout=_GIT_TIMEOUT, capture_output=True, env=git_env,
|
|
6827
|
+
)
|
|
6828
|
+
except Exception:
|
|
6829
|
+
_cleanup(temp_dir)
|
|
6830
|
+
raise
|
|
6831
|
+
|
|
6832
|
+
if skill_path and not (temp_dir / skill_path).exists():
|
|
6833
|
+
_cleanup(temp_dir)
|
|
6834
|
+
raise ValueError(f"Skill path '{skill_path}' not found in repository '{url}'")
|
|
6835
|
+
|
|
6836
|
+
# SKILL.md lives inside the subdir for sparse checkouts.
|
|
6837
|
+
skill_root = temp_dir / skill_path if skill_path else temp_dir
|
|
6838
|
+
label = f"{url}:{skill_path}" if skill_path else url
|
|
6839
|
+
final_dir = _rename_and_cache_skill(type_dir, temp_dir, source_hash, skill_root, source_label=label)
|
|
6840
|
+
return final_dir / skill_path if skill_path else final_dir
|
|
6841
|
+
|
|
6842
|
+
|
|
6843
|
+
def resolve_s3_skills(sources: list, s3_client=None) -> list:
|
|
6844
|
+
"""Resolve s3:// skill URIs to local filesystem paths.
|
|
6845
|
+
|
|
6846
|
+
Any fetch failure raises and fails the invocation — a partial skill set
|
|
6847
|
+
would silently run the agent without capabilities the harness declared.
|
|
6848
|
+
"""
|
|
6849
|
+
paths = []
|
|
6850
|
+
for uri in sources:
|
|
6851
|
+
try:
|
|
6852
|
+
skill_dir = _fetch_s3_skill(uri, s3_client)
|
|
6853
|
+
except Exception as e:
|
|
6854
|
+
raise ValueError(f"Failed to resolve S3 skill '{uri}': {e}") from e
|
|
6855
|
+
paths.append(str(skill_dir.resolve()))
|
|
6856
|
+
return paths
|
|
6857
|
+
|
|
6858
|
+
|
|
6859
|
+
def resolve_git_skills(sources: list, identity_client=None) -> list:
|
|
6860
|
+
"""Resolve git skill dicts to local filesystem paths.
|
|
6861
|
+
|
|
6862
|
+
Each source is a dict with keys: url (required), path (optional),
|
|
6863
|
+
credentialArn (optional), username (optional).
|
|
6864
|
+
|
|
6865
|
+
Any fetch failure raises and fails the invocation — a partial skill set
|
|
6866
|
+
would silently run the agent without capabilities the harness declared.
|
|
6867
|
+
"""
|
|
6868
|
+
paths = []
|
|
6869
|
+
for source in sources:
|
|
6870
|
+
try:
|
|
6871
|
+
skill_dir = _fetch_git_skill(
|
|
6872
|
+
url=source["url"],
|
|
6873
|
+
skill_path=source.get("path") or "",
|
|
6874
|
+
credential_arn=source.get("credentialArn"),
|
|
6875
|
+
username=source.get("username"),
|
|
6876
|
+
identity_client=identity_client,
|
|
6877
|
+
)
|
|
6878
|
+
except Exception as e:
|
|
6879
|
+
raise ValueError(f"Failed to resolve git skill '{source.get('url', source)}': {e}") from e
|
|
6880
|
+
paths.append(str(skill_dir.resolve()))
|
|
6881
|
+
return paths
|
|
6882
|
+
"
|
|
6883
|
+
`;
|
|
6884
|
+
|
|
6885
|
+
exports[`Assets Directory Snapshots > Python framework assets > python/python/http/strands/capabilities/execution-limits/hooks/execution_limits.py should match snapshot 1`] = `
|
|
6886
|
+
"import time
|
|
6887
|
+
from typing import Optional
|
|
6888
|
+
|
|
6889
|
+
from strands.hooks import BeforeModelCallEvent
|
|
6890
|
+
from strands.hooks.registry import HookProvider, HookRegistry
|
|
6891
|
+
from strands.types.exceptions import EventLoopException
|
|
6892
|
+
|
|
6893
|
+
|
|
6894
|
+
class ExecutionLimitExceeded(Exception):
|
|
6895
|
+
def __init__(self, message: str) -> None:
|
|
6896
|
+
super().__init__(message)
|
|
6897
|
+
|
|
6898
|
+
|
|
6899
|
+
class ExecutionLimitsHook(HookProvider):
|
|
6900
|
+
def __init__(
|
|
6901
|
+
self,
|
|
6902
|
+
max_iterations: Optional[int] = None,
|
|
6903
|
+
max_tokens: Optional[int] = None,
|
|
6904
|
+
timeout_seconds: Optional[float] = None,
|
|
6905
|
+
) -> None:
|
|
6906
|
+
self._max_iterations = max_iterations
|
|
6907
|
+
self._max_tokens = max_tokens
|
|
6908
|
+
self._timeout_seconds = timeout_seconds
|
|
6909
|
+
self._iteration_count = 0
|
|
6910
|
+
self._start_time = time.monotonic()
|
|
6911
|
+
|
|
6912
|
+
def register_hooks(self, registry: HookRegistry, **kwargs) -> None:
|
|
6913
|
+
registry.add_callback(BeforeModelCallEvent, self._check_limits)
|
|
6914
|
+
|
|
6915
|
+
def _check_limits(self, event: BeforeModelCallEvent) -> None:
|
|
6916
|
+
self._iteration_count += 1
|
|
6917
|
+
|
|
6918
|
+
if self._max_iterations is not None and self._iteration_count > self._max_iterations:
|
|
6919
|
+
raise EventLoopException(
|
|
6920
|
+
ExecutionLimitExceeded(f"Max iterations exceeded: {self._max_iterations}")
|
|
6921
|
+
)
|
|
6922
|
+
|
|
6923
|
+
if self._timeout_seconds is not None:
|
|
6924
|
+
elapsed = time.monotonic() - self._start_time
|
|
6925
|
+
if elapsed > self._timeout_seconds:
|
|
6926
|
+
raise EventLoopException(
|
|
6927
|
+
ExecutionLimitExceeded(
|
|
6928
|
+
f"Timeout exceeded: {self._timeout_seconds}s (elapsed {elapsed:.1f}s)"
|
|
6929
|
+
)
|
|
6930
|
+
)
|
|
6931
|
+
|
|
6932
|
+
if self._max_tokens is not None:
|
|
6933
|
+
used = event.agent.event_loop_metrics.accumulated_usage.get("outputTokens", 0)
|
|
6934
|
+
if used >= self._max_tokens:
|
|
6935
|
+
raise EventLoopException(
|
|
6936
|
+
ExecutionLimitExceeded(
|
|
6937
|
+
f"Max output tokens exceeded: {used}/{self._max_tokens}"
|
|
6938
|
+
)
|
|
6939
|
+
)
|
|
6940
|
+
"
|
|
6941
|
+
`;
|
|
6942
|
+
|
|
5061
6943
|
exports[`Assets Directory Snapshots > Python framework assets > python/python/http/strands/capabilities/memory/__init__.py should match snapshot 1`] = `
|
|
5062
6944
|
"# Package marker
|
|
5063
6945
|
"
|
|
@@ -5090,8 +6972,11 @@ def get_memory_session_manager(session_id: Optional[str], actor_id: str) -> Opti
|
|
|
5090
6972
|
{{#if (includes memoryProviders.[0].strategies "USER_PREFERENCE")}}
|
|
5091
6973
|
f"/users/{actor_id}/preferences": RetrievalConfig(top_k=3, relevance_score=0.5),
|
|
5092
6974
|
{{/if}}
|
|
6975
|
+
{{#if (includes memoryProviders.[0].strategies "EPISODIC")}}
|
|
6976
|
+
f"/episodes/{actor_id}/{session_id}": RetrievalConfig(top_k=5, relevance_score=0.5),
|
|
6977
|
+
{{/if}}
|
|
5093
6978
|
{{#if (includes memoryProviders.[0].strategies "SUMMARIZATION")}}
|
|
5094
|
-
f"/summaries/{actor_id}
|
|
6979
|
+
f"/summaries/{actor_id}": RetrievalConfig(top_k=3, relevance_score=0.5),
|
|
5095
6980
|
{{/if}}
|
|
5096
6981
|
}
|
|
5097
6982
|
{{/if}}
|
|
@@ -5111,6 +6996,160 @@ def get_memory_session_manager(session_id: Optional[str], actor_id: str) -> Opti
|
|
|
5111
6996
|
"
|
|
5112
6997
|
`;
|
|
5113
6998
|
|
|
6999
|
+
exports[`Assets Directory Snapshots > Python framework assets > python/python/http/strands/capabilities/payments/__init__.py should match snapshot 1`] = `
|
|
7000
|
+
""""Payment capabilities for Strands agents."""
|
|
7001
|
+
from .payments import create_payments_plugin
|
|
7002
|
+
|
|
7003
|
+
__all__ = ["create_payments_plugin"]
|
|
7004
|
+
"
|
|
7005
|
+
`;
|
|
7006
|
+
|
|
7007
|
+
exports[`Assets Directory Snapshots > Python framework assets > python/python/http/strands/capabilities/payments/payments.py should match snapshot 1`] = `
|
|
7008
|
+
""""Payment capability -- auto-generated by agentcore CLI.
|
|
7009
|
+
|
|
7010
|
+
Configures AgentCorePaymentsPlugin for Strands agents.
|
|
7011
|
+
Manager config is auto-discovered from AGENTCORE_PAYMENT_*
|
|
7012
|
+
environment variables set at deploy time.
|
|
7013
|
+
|
|
7014
|
+
Uses a per-invocation factory pattern so each request gets its own
|
|
7015
|
+
plugin instance with the correct user_id, instrument_id, and session_id.
|
|
7016
|
+
This prevents concurrency bugs where one user's payment context
|
|
7017
|
+
could leak to another user's request.
|
|
7018
|
+
|
|
7019
|
+
Uses the official SDK plugin which handles:
|
|
7020
|
+
- x402 v1 (body-based) and v2 (header-based) payment detection
|
|
7021
|
+
- Automatic 402 response interception and payment processing
|
|
7022
|
+
- Retry limiting (max 3 payment retries per tool use)
|
|
7023
|
+
- Error management and logging
|
|
7024
|
+
"""
|
|
7025
|
+
import os
|
|
7026
|
+
import logging
|
|
7027
|
+
|
|
7028
|
+
import boto3
|
|
7029
|
+
from bedrock_agentcore.payments.integrations.strands import AgentCorePaymentsPlugin
|
|
7030
|
+
from bedrock_agentcore.payments.integrations.config import AgentCorePaymentsPluginConfig
|
|
7031
|
+
|
|
7032
|
+
logger = logging.getLogger(__name__)
|
|
7033
|
+
|
|
7034
|
+
PAYMENT_SYSTEM_PROMPT = """
|
|
7035
|
+
You have payment capabilities via the x402 protocol:
|
|
7036
|
+
- Use http_request to call HTTP endpoints. 402 Payment Required responses are settled automatically by the plugin and the call is retried.
|
|
7037
|
+
- Use get_payment_session to check your remaining budget before expensive operations
|
|
7038
|
+
- Use get_payment_instrument_balance to check wallet USDC balance
|
|
7039
|
+
- Use list_payment_instruments to see available payment instruments
|
|
7040
|
+
- If budget is low, inform the user before proceeding with paid requests
|
|
7041
|
+
"""
|
|
7042
|
+
|
|
7043
|
+
_manager_arn = None
|
|
7044
|
+
_connector_id = None
|
|
7045
|
+
_process_payment_role_arn = None
|
|
7046
|
+
_name_segment = None
|
|
7047
|
+
_region = None
|
|
7048
|
+
_auth_mode = None
|
|
7049
|
+
_manager_count = 0
|
|
7050
|
+
for key, value in os.environ.items():
|
|
7051
|
+
if key.startswith("AGENTCORE_PAYMENT_") and key.endswith("_MANAGER_ARN"):
|
|
7052
|
+
if _manager_arn is None:
|
|
7053
|
+
_manager_arn = value
|
|
7054
|
+
_name_segment = key[len("AGENTCORE_PAYMENT_"):-len("_MANAGER_ARN")]
|
|
7055
|
+
_manager_count += 1
|
|
7056
|
+
if _manager_count > 1:
|
|
7057
|
+
logger.warning(
|
|
7058
|
+
"Multiple payment managers detected in environment. Using the first one found. "
|
|
7059
|
+
"Remove extra AGENTCORE_PAYMENT_*_MANAGER_ARN env vars to eliminate ambiguity."
|
|
7060
|
+
)
|
|
7061
|
+
_region = os.getenv("AWS_REGION")
|
|
7062
|
+
|
|
7063
|
+
_prefix = f"AGENTCORE_PAYMENT_{_name_segment}_" if _name_segment else "AGENTCORE_PAYMENT_"
|
|
7064
|
+
_auth_mode = os.getenv(f"{_prefix}AUTH_MODE", "sigv4")
|
|
7065
|
+
_connector_id = os.getenv(f"{_prefix}CONNECTOR_ID")
|
|
7066
|
+
_process_payment_role_arn = os.getenv(f"{_prefix}PROCESS_PAYMENT_ROLE_ARN")
|
|
7067
|
+
_auto_payment = os.getenv(f"{_prefix}AUTO_PAYMENT", "true").lower() == "true"
|
|
7068
|
+
_allowlist_raw = os.getenv(f"{_prefix}TOOL_ALLOWLIST")
|
|
7069
|
+
_allowlist = _allowlist_raw.split(",") if _allowlist_raw else None
|
|
7070
|
+
_network_prefs_raw = os.getenv(f"{_prefix}NETWORK_PREFERENCES")
|
|
7071
|
+
_network_prefs = _network_prefs_raw.split(",") if _network_prefs_raw else None
|
|
7072
|
+
|
|
7073
|
+
if not _manager_arn:
|
|
7074
|
+
logger.warning("No payment manager config found in environment")
|
|
7075
|
+
if not _connector_id:
|
|
7076
|
+
logger.warning("No payment connector config found in environment")
|
|
7077
|
+
|
|
7078
|
+
|
|
7079
|
+
def _assume_role_session(role_arn):
|
|
7080
|
+
"""Assume an IAM role and return a boto3 session with temporary credentials."""
|
|
7081
|
+
sts = boto3.client("sts", region_name=_region)
|
|
7082
|
+
creds = sts.assume_role(
|
|
7083
|
+
RoleArn=role_arn,
|
|
7084
|
+
RoleSessionName="agentcore-payment-plugin",
|
|
7085
|
+
)["Credentials"]
|
|
7086
|
+
return boto3.Session(
|
|
7087
|
+
aws_access_key_id=creds["AccessKeyId"],
|
|
7088
|
+
aws_secret_access_key=creds["SecretAccessKey"],
|
|
7089
|
+
aws_session_token=creds["SessionToken"],
|
|
7090
|
+
region_name=_region,
|
|
7091
|
+
)
|
|
7092
|
+
|
|
7093
|
+
|
|
7094
|
+
def create_payments_plugin(user_id, instrument_id=None, session_id=None):
|
|
7095
|
+
"""Create a fresh plugin instance per invocation.
|
|
7096
|
+
|
|
7097
|
+
Args:
|
|
7098
|
+
user_id: From invocation context (required for SigV4, derived from JWT for bearer)
|
|
7099
|
+
instrument_id: From invocation payload (created by app backend per user)
|
|
7100
|
+
session_id: From invocation payload (created by app backend per conversation)
|
|
7101
|
+
|
|
7102
|
+
Returns:
|
|
7103
|
+
AgentCorePaymentsPlugin instance, or None if no manager is configured.
|
|
7104
|
+
"""
|
|
7105
|
+
if not _manager_arn:
|
|
7106
|
+
return None
|
|
7107
|
+
|
|
7108
|
+
config_kwargs = {
|
|
7109
|
+
"payment_manager_arn": _manager_arn,
|
|
7110
|
+
"region": _region,
|
|
7111
|
+
"payment_instrument_id": instrument_id,
|
|
7112
|
+
"payment_session_id": session_id,
|
|
7113
|
+
"payment_connector_id": _connector_id,
|
|
7114
|
+
}
|
|
7115
|
+
|
|
7116
|
+
config_kwargs["auto_payment"] = _auto_payment
|
|
7117
|
+
if _allowlist:
|
|
7118
|
+
config_kwargs["payment_tool_allowlist"] = _allowlist
|
|
7119
|
+
if _network_prefs:
|
|
7120
|
+
config_kwargs["network_preferences_config"] = _network_prefs
|
|
7121
|
+
|
|
7122
|
+
if _process_payment_role_arn:
|
|
7123
|
+
# Only pass boto3_session if SDK supports it (added in bedrock-agentcore >= 1.11).
|
|
7124
|
+
# Older SDKs use the runtime role's default credentials and can still call ProcessPayment
|
|
7125
|
+
# if the runtime role has been granted permission directly.
|
|
7126
|
+
import inspect
|
|
7127
|
+
if "boto3_session" in inspect.signature(AgentCorePaymentsPluginConfig).parameters:
|
|
7128
|
+
config_kwargs["boto3_session"] = _assume_role_session(_process_payment_role_arn)
|
|
7129
|
+
else:
|
|
7130
|
+
logger.warning(
|
|
7131
|
+
"PROCESS_PAYMENT_ROLE_ARN set but bedrock-agentcore SDK does not support boto3_session. "
|
|
7132
|
+
"Upgrade to bedrock-agentcore>=1.11 to enable cross-role payment processing."
|
|
7133
|
+
)
|
|
7134
|
+
|
|
7135
|
+
if _auth_mode == "bearer":
|
|
7136
|
+
bearer_token = os.getenv("AGENTCORE_BEARER_TOKEN")
|
|
7137
|
+
if bearer_token:
|
|
7138
|
+
config_kwargs["bearer_token"] = bearer_token
|
|
7139
|
+
else:
|
|
7140
|
+
logger.warning(
|
|
7141
|
+
"Bearer auth mode configured but AGENTCORE_BEARER_TOKEN not set. "
|
|
7142
|
+
"Falling back to SigV4. Set AGENTCORE_BEARER_TOKEN or pass bearer_token in invoke context."
|
|
7143
|
+
)
|
|
7144
|
+
config_kwargs["user_id"] = user_id or "default-user"
|
|
7145
|
+
else:
|
|
7146
|
+
config_kwargs["user_id"] = user_id or "default-user"
|
|
7147
|
+
|
|
7148
|
+
config = AgentCorePaymentsPluginConfig(**config_kwargs)
|
|
7149
|
+
return AgentCorePaymentsPlugin(config=config)
|
|
7150
|
+
"
|
|
7151
|
+
`;
|
|
7152
|
+
|
|
5114
7153
|
exports[`Assets Directory Snapshots > Python framework assets > python/python/mcp/standalone/base/README.md should match snapshot 1`] = `
|
|
5115
7154
|
"# {{ name }}
|
|
5116
7155
|
|
|
@@ -5197,10 +7236,68 @@ Thumbs.db
|
|
|
5197
7236
|
`;
|
|
5198
7237
|
|
|
5199
7238
|
exports[`Assets Directory Snapshots > Python framework assets > python/python/mcp/standalone/base/main.py should match snapshot 1`] = `
|
|
5200
|
-
"
|
|
7239
|
+
"{{#if needsOs}}
|
|
7240
|
+
import os
|
|
7241
|
+
{{/if}}
|
|
7242
|
+
from mcp.server.fastmcp import FastMCP
|
|
5201
7243
|
|
|
5202
7244
|
mcp = FastMCP("{{ name }}", host="0.0.0.0", stateless_http=True)
|
|
5203
7245
|
|
|
7246
|
+
{{#if needsOs}}
|
|
7247
|
+
_MOUNT_PATHS = [
|
|
7248
|
+
{{#if sessionStorageMountPath}}"{{sessionStorageMountPath}}",{{/if}}
|
|
7249
|
+
{{#each efsMounts}}"{{mountPath}}",{{/each}}
|
|
7250
|
+
{{#each s3Mounts}}"{{mountPath}}",{{/each}}
|
|
7251
|
+
]
|
|
7252
|
+
|
|
7253
|
+
def _safe_resolve(path: str) -> str:
|
|
7254
|
+
resolved = os.path.realpath(path)
|
|
7255
|
+
if not any(resolved == os.path.realpath(m) or resolved.startswith(os.path.realpath(m) + os.sep) for m in _MOUNT_PATHS):
|
|
7256
|
+
raise ValueError(f"Path '{path}' is not within any configured mount ({', '.join(_MOUNT_PATHS)})")
|
|
7257
|
+
return resolved
|
|
7258
|
+
|
|
7259
|
+
@mcp.tool()
|
|
7260
|
+
def file_read(path: str) -> str:
|
|
7261
|
+
"""Read a file from a mounted filesystem. Use the absolute path (e.g. /mnt/tools/data.txt)."""
|
|
7262
|
+
try:
|
|
7263
|
+
full_path = _safe_resolve(path)
|
|
7264
|
+
with open(full_path) as f:
|
|
7265
|
+
return f.read()
|
|
7266
|
+
except ValueError as e:
|
|
7267
|
+
return str(e)
|
|
7268
|
+
except OSError as e:
|
|
7269
|
+
return f"Error reading '{path}': {e.strerror}"
|
|
7270
|
+
|
|
7271
|
+
@mcp.tool()
|
|
7272
|
+
def file_write(path: str, content: str) -> str:
|
|
7273
|
+
"""Write a file to a mounted filesystem. Use the absolute path (e.g. /mnt/tools/data.txt)."""
|
|
7274
|
+
try:
|
|
7275
|
+
full_path = _safe_resolve(path)
|
|
7276
|
+
parent = os.path.dirname(full_path)
|
|
7277
|
+
if parent:
|
|
7278
|
+
os.makedirs(parent, exist_ok=True)
|
|
7279
|
+
with open(full_path, "w") as f:
|
|
7280
|
+
f.write(content)
|
|
7281
|
+
return f"Written to {path}"
|
|
7282
|
+
except ValueError as e:
|
|
7283
|
+
return str(e)
|
|
7284
|
+
except OSError as e:
|
|
7285
|
+
return f"Error writing '{path}': {e.strerror}"
|
|
7286
|
+
|
|
7287
|
+
@mcp.tool()
|
|
7288
|
+
def list_files(path: str) -> str:
|
|
7289
|
+
"""List files in a mounted filesystem directory. Use the absolute path (e.g. /mnt/tools)."""
|
|
7290
|
+
try:
|
|
7291
|
+
full_path = _safe_resolve(path)
|
|
7292
|
+
entries = os.listdir(full_path)
|
|
7293
|
+
return "\\n".join(entries) if entries else "(empty directory)"
|
|
7294
|
+
except ValueError as e:
|
|
7295
|
+
return str(e)
|
|
7296
|
+
except OSError as e:
|
|
7297
|
+
return f"Error listing '{path}': {e.strerror}"
|
|
7298
|
+
|
|
7299
|
+
{{/if}}
|
|
7300
|
+
|
|
5204
7301
|
|
|
5205
7302
|
@mcp.tool()
|
|
5206
7303
|
def add_numbers(a: int, b: int) -> int:
|
|
@@ -5237,7 +7334,7 @@ description = "AgentCore MCP Server"
|
|
|
5237
7334
|
readme = "README.md"
|
|
5238
7335
|
requires-python = ">=3.10"
|
|
5239
7336
|
dependencies = [
|
|
5240
|
-
"mcp
|
|
7337
|
+
"mcp ~= 1.24.0",
|
|
5241
7338
|
]
|
|
5242
7339
|
|
|
5243
7340
|
[tool.hatch.build.targets.wheel]
|
|
@@ -5305,7 +7402,7 @@ For Container builds, \`BaseRenderer.render()\` automatically copies the \`conta
|
|
|
5305
7402
|
|
|
5306
7403
|
- These assets are rendered by the CLI's template renderer in \`src/cli/templates/\`
|
|
5307
7404
|
- The \`README.md\` and \`agents/AGENTS.md\` are copied verbatim (no template rendering) during project creation
|
|
5308
|
-
- The \`.llm-context/\` files are sourced from \`src/schema/llm-compacted/\` and written during
|
|
7405
|
+
- The \`.llm-context/\` files are sourced from \`src/schema/llm-compacted/\` and written during project creation
|
|
5309
7406
|
"
|
|
5310
7407
|
`;
|
|
5311
7408
|
|
|
@@ -5325,8 +7422,7 @@ my-project/
|
|
|
5325
7422
|
│ ├── .env.local # Secrets — API keys (gitignored)
|
|
5326
7423
|
│ ├── .llm-context/ # TypeScript type definitions for AI assistants
|
|
5327
7424
|
│ │ ├── agentcore.ts # AgentCoreProjectSpec types
|
|
5328
|
-
│ │
|
|
5329
|
-
│ │ └── mcp.ts # Gateway and MCP tool types
|
|
7425
|
+
│ │ └── aws-targets.ts # Deployment target types
|
|
5330
7426
|
│ └── cdk/ # CDK infrastructure (@aws/agentcore-cdk)
|
|
5331
7427
|
├── app/ # Agent application code
|
|
5332
7428
|
└── evaluators/ # Custom evaluator code (if any)
|
|
@@ -5349,6 +7445,11 @@ Run your agent locally:
|
|
|
5349
7445
|
agentcore dev
|
|
5350
7446
|
\`\`\`
|
|
5351
7447
|
|
|
7448
|
+
### Validate Invocation Input
|
|
7449
|
+
|
|
7450
|
+
Validate runtime invocation payloads before forwarding them to an agent framework. Keep user prompts typed as strings
|
|
7451
|
+
and pass only prompt text to the agent.
|
|
7452
|
+
|
|
5352
7453
|
### Deployment
|
|
5353
7454
|
|
|
5354
7455
|
Deploy to AWS:
|
|
@@ -5443,6 +7544,8 @@ Tags defined in \`agentcore.json\` flow through to deployed CloudFormation resou
|
|
|
5443
7544
|
\`agentcore validate\` to check.
|
|
5444
7545
|
4. **Resource Removal:** Use \`agentcore remove\` to remove resources. Run \`agentcore deploy\` after removal to tear down
|
|
5445
7546
|
deployed infrastructure.
|
|
7547
|
+
5. **Invocation Input:** Validate runtime payloads and require text prompts to be strings. If a Strands app accepts a
|
|
7548
|
+
caller-supplied message history, normalize the history tail with \`strip_trailing_tool_use()\` before invocation.
|
|
5446
7549
|
|
|
5447
7550
|
## Directory Structure
|
|
5448
7551
|
|
|
@@ -5456,8 +7559,7 @@ myProject/
|
|
|
5456
7559
|
│ ├── .llm-context/ # TypeScript type definitions for AI assistants
|
|
5457
7560
|
│ │ ├── README.md # Guide to using schema files
|
|
5458
7561
|
│ │ ├── agentcore.ts # AgentCoreProjectSpec types
|
|
5459
|
-
│ │
|
|
5460
|
-
│ │ └── mcp.ts # Gateway and MCP tool types
|
|
7562
|
+
│ │ └── aws-targets.ts # AWS deployment target types
|
|
5461
7563
|
│ └── cdk/ # AWS CDK project (@aws/agentcore-cdk L3 constructs)
|
|
5462
7564
|
├── app/ # Agent application code
|
|
5463
7565
|
└── evaluators/ # Custom evaluator code (if any)
|
|
@@ -5471,18 +7573,25 @@ file maps to a JSON config file and includes validation constraints as comments
|
|
|
5471
7573
|
| JSON Config | Schema File | Root Type |
|
|
5472
7574
|
| --- | --- | --- |
|
|
5473
7575
|
| \`agentcore/agentcore.json\` | \`agentcore/.llm-context/agentcore.ts\` | \`AgentCoreProjectSpec\` |
|
|
5474
|
-
| \`agentcore/agentcore.json\` (gateways) | \`agentcore/.llm-context/mcp.ts\` | \`AgentCoreMcpSpec\` |
|
|
5475
7576
|
| \`agentcore/aws-targets.json\` | \`agentcore/.llm-context/aws-targets.ts\` | \`AwsDeploymentTarget[]\` |
|
|
5476
7577
|
|
|
5477
7578
|
### Key Types
|
|
5478
7579
|
|
|
5479
|
-
- **AgentCoreProjectSpec**: Root config with
|
|
7580
|
+
- **AgentCoreProjectSpec**: Root config with runtimes, memories, knowledge bases, credentials, evaluators, online evals and insights, gateways, policy engines, config bundles, A/B tests, harness registrations, datasets, payment managers, and capacity providers
|
|
5480
7581
|
- **AgentEnvSpec**: Agent configuration (build type, entrypoint, code location, runtime version, network mode)
|
|
5481
7582
|
- **Memory**: Memory resource with strategies (SEMANTIC, SUMMARIZATION, USER_PREFERENCE, EPISODIC) and expiry
|
|
5482
7583
|
- **Credential**: API key or OAuth credential provider
|
|
5483
|
-
- **AgentCoreGateway**: MCP gateway with targets (Lambda, MCP server, OpenAPI, Smithy, API Gateway)
|
|
7584
|
+
- **AgentCoreGateway**: MCP gateway with targets (Lambda, MCP server, OpenAPI, Smithy, API Gateway, web-search, knowledge-base)
|
|
5484
7585
|
- **Evaluator**: LLM-as-a-Judge or code-based evaluator
|
|
5485
7586
|
- **OnlineEvalConfig**: Continuous evaluation pipeline bound to an agent
|
|
7587
|
+
- **OnlineInsightsConfig** _[preview]_: Continuous failure-pattern analysis bound to an agent
|
|
7588
|
+
- **KnowledgeBase**: Managed Bedrock Knowledge Base auto-wired to a gateway
|
|
7589
|
+
- **Harness**: Declarative agent — runtime + tools + skills + memory + observability without writing agent code
|
|
7590
|
+
- **PolicyEngine** + **Policy**: Cedar policy engine with form-based guardrails (Bedrock content filters, prompt-attack, sensitive-info) or raw Cedar policies
|
|
7591
|
+
- **PaymentManager** + **PaymentConnector**: x402-protocol payment orchestration with provider credentials (CoinbaseCDP, StripePrivy)
|
|
7592
|
+
- **ConfigBundle**: Versioned runtime configuration as a separately-deployable resource
|
|
7593
|
+
- **Dataset**: Curated session dataset for batch evaluation and recommendation runs
|
|
7594
|
+
- **RuntimeEndpoint**: Named endpoint (e.g. \`PROMPT_V1\`) targeting a specific runtime version
|
|
5486
7595
|
|
|
5487
7596
|
### Common Enum Values
|
|
5488
7597
|
|
|
@@ -5490,8 +7599,11 @@ file maps to a JSON config file and includes validation constraints as comments
|
|
|
5490
7599
|
- **NetworkMode**: \`'PUBLIC'\` | \`'VPC'\`
|
|
5491
7600
|
- **RuntimeVersion**: \`'PYTHON_3_10'\` | \`'PYTHON_3_11'\` | \`'PYTHON_3_12'\` | \`'PYTHON_3_13'\` | \`'PYTHON_3_14'\` | \`'NODE_18'\` | \`'NODE_20'\` | \`'NODE_22'\`
|
|
5492
7601
|
- **MemoryStrategyType**: \`'SEMANTIC'\` | \`'SUMMARIZATION'\` | \`'USER_PREFERENCE'\` | \`'EPISODIC'\`
|
|
5493
|
-
- **GatewayTargetType**: \`'lambda'\` | \`'mcpServer'\` | \`'openApiSchema'\` | \`'smithyModel'\` | \`'apiGateway'\` | \`'lambdaFunctionArn'\`
|
|
7602
|
+
- **GatewayTargetType**: \`'lambda'\` | \`'mcpServer'\` | \`'openApiSchema'\` | \`'smithyModel'\` | \`'apiGateway'\` | \`'lambdaFunctionArn'\` | \`'connector'\` (web-search, bedrock-knowledge-bases)
|
|
5494
7603
|
- **ModelProvider**: \`'Bedrock'\` | \`'Gemini'\` | \`'OpenAI'\` | \`'Anthropic'\`
|
|
7604
|
+
- **PaymentProvider**: \`'CoinbaseCDP'\` | \`'StripePrivy'\`
|
|
7605
|
+
- **PolicyEnforcementMode**: \`'ACTIVE'\` | \`'PASSIVE'\`
|
|
7606
|
+
- **GuardrailContentFilter**: \`'VIOLENCE'\` | \`'HATE'\` | \`'SEXUAL'\` | \`'MISCONDUCT'\` | \`'INSULTS'\`
|
|
5495
7607
|
|
|
5496
7608
|
### Build Types
|
|
5497
7609
|
|
|
@@ -5542,24 +7654,890 @@ When modifying JSON config files:
|
|
|
5542
7654
|
4. Use CloudFormation-safe names (alphanumeric, start with letter)
|
|
5543
7655
|
5. Run \`agentcore validate\` to verify changes
|
|
5544
7656
|
|
|
7657
|
+
## Harness Export
|
|
7658
|
+
|
|
7659
|
+
\`agentcore export harness\` converts a harness configuration into a deployable Strands Python agent under \`app/<agentName>/\`.
|
|
7660
|
+
|
|
7661
|
+
**After every export, you MUST read \`app/<agentName>/EXPORT_NOTES.md\` before proceeding.**
|
|
7662
|
+
|
|
7663
|
+
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\`.
|
|
7664
|
+
|
|
7665
|
+
\`\`\`bash
|
|
7666
|
+
agentcore export harness --name <harnessName> # generates app/<agentName>/EXPORT_NOTES.md
|
|
7667
|
+
cat app/<agentName>/EXPORT_NOTES.md # read this before touching anything else
|
|
7668
|
+
\`\`\`
|
|
7669
|
+
|
|
5545
7670
|
## CLI Commands
|
|
5546
7671
|
|
|
7672
|
+
Run \`agentcore --help\` or \`agentcore <command> --help\` for full flags. Commonly used:
|
|
7673
|
+
|
|
7674
|
+
**Project lifecycle**
|
|
7675
|
+
|
|
5547
7676
|
| Command | Description |
|
|
5548
7677
|
| --- | --- |
|
|
5549
7678
|
| \`agentcore create\` | Create a new project |
|
|
5550
|
-
| \`agentcore add <resource>\` | Add agent, memory, credential, gateway, evaluator, policy |
|
|
5551
|
-
| \`agentcore remove <resource>\` | Remove a resource |
|
|
5552
7679
|
| \`agentcore dev\` | Run agent locally with hot-reload |
|
|
5553
7680
|
| \`agentcore deploy\` | Deploy to AWS |
|
|
5554
|
-
| \`agentcore status\` | Show deployment status |
|
|
5555
7681
|
| \`agentcore invoke\` | Invoke agent (local or deployed) |
|
|
5556
|
-
| \`agentcore
|
|
5557
|
-
| \`agentcore
|
|
5558
|
-
| \`agentcore eval\` | Run evaluations against an agent |
|
|
5559
|
-
| \`agentcore package\` | Package agent artifacts |
|
|
7682
|
+
| \`agentcore capacity-provider delete-session\` | Delete (deprovision) a live capacity provider session |
|
|
7683
|
+
| \`agentcore status\` | Show deployment status |
|
|
5560
7684
|
| \`agentcore validate\` | Validate configuration |
|
|
5561
|
-
| \`agentcore
|
|
7685
|
+
| \`agentcore package\` | Package agent artifacts |
|
|
7686
|
+
| \`agentcore import\` | Import resources from a Bedrock AgentCore Starter Toolkit project |
|
|
7687
|
+
|
|
7688
|
+
**Resources**
|
|
7689
|
+
|
|
7690
|
+
| Command | Description |
|
|
7691
|
+
| --- | --- |
|
|
7692
|
+
| \`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, capacity-provider, config-bundle, dataset, runtime-endpoint |
|
|
7693
|
+
| \`agentcore remove <resource>\` | Remove any resource |
|
|
7694
|
+
| \`agentcore export harness\` | Export a harness to a Strands runtime agent under \`app/<agentName>/\` |
|
|
7695
|
+
|
|
7696
|
+
**Jobs (run, view, archive, lifecycle)**
|
|
7697
|
+
|
|
7698
|
+
| Command | Description |
|
|
7699
|
+
| --- | --- |
|
|
7700
|
+
| \`agentcore run eval\` | Run on-demand evaluation against agent traces |
|
|
7701
|
+
| \`agentcore run batch-evaluation\` | Run evaluators across all sessions at scale |
|
|
7702
|
+
| \`agentcore run recommendation\` | Optimize prompts or tool descriptions from real traces |
|
|
7703
|
+
| \`agentcore run insights\` _[preview]_ | Run failure-pattern analysis across sessions |
|
|
7704
|
+
| \`agentcore run ab-test\` | Start an A/B test (config-bundle or target-based) |
|
|
7705
|
+
| \`agentcore run ingest\` | Start a fresh ingestion job for every data source on a deployed knowledge base |
|
|
7706
|
+
| \`agentcore view <type>\` | List or view jobs (recommendation, batch-evaluation, ab-test, insights) |
|
|
7707
|
+
| \`agentcore archive <type>\` | Delete a job on the service + clear local history |
|
|
7708
|
+
| \`agentcore stop <type>\` | Stop a running batch-evaluation or ab-test |
|
|
7709
|
+
| \`agentcore promote ab-test\` | Apply the winning variant to \`agentcore.json\` |
|
|
7710
|
+
| \`agentcore pause <type>\` / \`agentcore resume <type>\` | Pause/resume a deployed online-eval, online-insights, or ab-test |
|
|
7711
|
+
|
|
7712
|
+
**Config bundles & datasets**
|
|
7713
|
+
|
|
7714
|
+
| Command | Description |
|
|
7715
|
+
| --- | --- |
|
|
7716
|
+
| \`agentcore config-bundle versions\` (alias \`cb versions\`) | List version history for a bundle |
|
|
7717
|
+
| \`agentcore config-bundle diff\` | Diff two versions of a bundle |
|
|
7718
|
+
| \`agentcore config-bundle create-branch\` | Create a new branch on an existing bundle |
|
|
7719
|
+
| \`agentcore dataset download\` | Download a dataset version locally |
|
|
7720
|
+
| \`agentcore dataset publish-version\` | Publish a new dataset version |
|
|
7721
|
+
| \`agentcore dataset remove-version\` | Remove a dataset version |
|
|
7722
|
+
|
|
7723
|
+
**Observability & history**
|
|
7724
|
+
|
|
7725
|
+
| Command | Description |
|
|
7726
|
+
| --- | --- |
|
|
7727
|
+
| \`agentcore logs\` | Stream/search agent runtime logs |
|
|
7728
|
+
| \`agentcore logs evals\` | Stream/search online-eval logs |
|
|
7729
|
+
| \`agentcore traces list\` / \`agentcore traces get\` | List recent traces or download one to JSON |
|
|
7730
|
+
| \`agentcore evals history\` | View past on-demand eval results |
|
|
7731
|
+
|
|
7732
|
+
**Utilities**
|
|
7733
|
+
|
|
7734
|
+
| Command | Description |
|
|
7735
|
+
| --- | --- |
|
|
7736
|
+
| \`agentcore fetch access\` | Fetch access info for deployed gateway or agent |
|
|
7737
|
+
| \`agentcore feedback\` | Send feedback (with optional screenshot) to the AgentCore team |
|
|
7738
|
+
| \`agentcore update\` | Check for and install CLI updates |
|
|
7739
|
+
| \`agentcore telemetry\` | View or change telemetry preferences |
|
|
5562
7740
|
"
|
|
5563
7741
|
`;
|
|
5564
7742
|
|
|
5565
7743
|
exports[`Assets Directory Snapshots > TypeScript assets > typescript/typescript/.gitkeep should match snapshot 1`] = `""`;
|
|
7744
|
+
|
|
7745
|
+
exports[`Assets Directory Snapshots > TypeScript assets > typescript/typescript/http/strands/base/README.md should match snapshot 1`] = `
|
|
7746
|
+
"This is a project generated by the AgentCore CLI!
|
|
7747
|
+
|
|
7748
|
+
# Layout
|
|
7749
|
+
|
|
7750
|
+
The generated application code lives at the agent root directory. At the root, there is a \`.gitignore\` file, an
|
|
7751
|
+
\`agentcore/\` folder which represents the configurations and state associated with this project. Other \`agentcore\`
|
|
7752
|
+
commands like \`deploy\`, \`dev\`, and \`invoke\` rely on the configuration stored here.
|
|
7753
|
+
|
|
7754
|
+
## Agent Root
|
|
7755
|
+
|
|
7756
|
+
The main entrypoint to your app is defined in \`main.ts\`. Using the AgentCore SDK \`BedrockAgentCoreApp\`, this file
|
|
7757
|
+
defines an HTTP server that streams tokens from your chosen Agent framework SDK.
|
|
7758
|
+
|
|
7759
|
+
\`model/load.ts\` instantiates your chosen model provider.
|
|
7760
|
+
|
|
7761
|
+
## Input Validation
|
|
7762
|
+
|
|
7763
|
+
The generated Zod request schema keeps plain prompts typed as strings before forwarding them to Strands. Retain this
|
|
7764
|
+
validation when extending the request shape, and pass only prompt text to the agent.
|
|
7765
|
+
|
|
7766
|
+
## Environment Variables
|
|
7767
|
+
|
|
7768
|
+
| Variable | Required | Description |
|
|
7769
|
+
| --- | --- | --- |
|
|
7770
|
+
{{#if hasIdentity}}| \`{{identityProviders.[0].envVarName}}\` | Yes | {{modelProvider}} API key (local) or Identity provider name (deployed) |
|
|
7771
|
+
{{/if}}| \`LOCAL_DEV\` | No | Set to \`1\` to use \`.env.local\` instead of AgentCore Identity |
|
|
7772
|
+
|
|
7773
|
+
# Developing locally
|
|
7774
|
+
|
|
7775
|
+
If installation was successful, \`node_modules/\` is already populated with dependencies.
|
|
7776
|
+
|
|
7777
|
+
\`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\`).
|
|
7778
|
+
|
|
7779
|
+
In a new terminal, you can invoke that server with:
|
|
7780
|
+
|
|
7781
|
+
\`agentcore invoke --dev "What can you do"\`
|
|
7782
|
+
|
|
7783
|
+
# Deployment
|
|
7784
|
+
|
|
7785
|
+
After providing credentials, \`agentcore deploy\` will deploy your project into Amazon Bedrock AgentCore.
|
|
7786
|
+
|
|
7787
|
+
Use \`agentcore invoke\` to invoke your deployed agent.
|
|
7788
|
+
"
|
|
7789
|
+
`;
|
|
7790
|
+
|
|
7791
|
+
exports[`Assets Directory Snapshots > TypeScript assets > typescript/typescript/http/strands/base/gitignore.template should match snapshot 1`] = `
|
|
7792
|
+
"# Environment variables
|
|
7793
|
+
.env
|
|
7794
|
+
.env.*
|
|
7795
|
+
|
|
7796
|
+
# Node
|
|
7797
|
+
node_modules/
|
|
7798
|
+
dist/
|
|
7799
|
+
*.log
|
|
7800
|
+
npm-debug.log*
|
|
7801
|
+
yarn-debug.log*
|
|
7802
|
+
yarn-error.log*
|
|
7803
|
+
|
|
7804
|
+
# IDE
|
|
7805
|
+
.vscode/
|
|
7806
|
+
.idea/
|
|
7807
|
+
*.swp
|
|
7808
|
+
*.swo
|
|
7809
|
+
*~
|
|
7810
|
+
|
|
7811
|
+
# OS
|
|
7812
|
+
.DS_Store
|
|
7813
|
+
Thumbs.db
|
|
7814
|
+
"
|
|
7815
|
+
`;
|
|
7816
|
+
|
|
7817
|
+
exports[`Assets Directory Snapshots > TypeScript assets > typescript/typescript/http/strands/base/main.ts should match snapshot 1`] = `
|
|
7818
|
+
"import { BedrockAgentCoreApp } from 'bedrock-agentcore/runtime';
|
|
7819
|
+
import { Agent, McpClient, tool, type ToolList } from '@strands-agents/sdk';
|
|
7820
|
+
import { z } from 'zod';
|
|
7821
|
+
import { loadModel } from './model/load.js';
|
|
7822
|
+
import { getStreamableHttpMcpClient } from './mcp_client/client.js';
|
|
7823
|
+
{{#if hasMemory}}
|
|
7824
|
+
import { getActorId, getOrCreateMemoryManager } from './memory/memory.js';
|
|
7825
|
+
{{/if}}
|
|
7826
|
+
|
|
7827
|
+
// Define a collection of MCP clients (filter out anything that failed to initialize)
|
|
7828
|
+
const mcpClients: McpClient[] = [getStreamableHttpMcpClient()].filter(
|
|
7829
|
+
(client): client is McpClient => Boolean(client)
|
|
7830
|
+
);
|
|
7831
|
+
|
|
7832
|
+
// Define a collection of tools used by the model
|
|
7833
|
+
const tools: ToolList = [];
|
|
7834
|
+
|
|
7835
|
+
// Define a simple function tool — the Zod schema gives us type inference and runtime validation for free
|
|
7836
|
+
const addNumbers = tool({
|
|
7837
|
+
name: 'add_numbers',
|
|
7838
|
+
description: 'Return the sum of two numbers',
|
|
7839
|
+
inputSchema: z.object({
|
|
7840
|
+
a: z.number(),
|
|
7841
|
+
b: z.number(),
|
|
7842
|
+
}),
|
|
7843
|
+
callback: async ({ a, b }) => a + b,
|
|
7844
|
+
});
|
|
7845
|
+
tools.push(addNumbers);
|
|
7846
|
+
|
|
7847
|
+
// Add MCP clients to tools
|
|
7848
|
+
tools.push(...mcpClients);
|
|
7849
|
+
|
|
7850
|
+
const SYSTEM_PROMPT = \`
|
|
7851
|
+
You are a helpful assistant. Use tools when appropriate.
|
|
7852
|
+
\`;
|
|
7853
|
+
|
|
7854
|
+
const requestSchema = z.object({
|
|
7855
|
+
prompt: z.string().default(''),
|
|
7856
|
+
});
|
|
7857
|
+
|
|
7858
|
+
{{#if hasMemory}}
|
|
7859
|
+
const agentCache = new Map<string, Agent>();
|
|
7860
|
+
|
|
7861
|
+
async function getOrCreateAgent(sessionId: string, actorId: string): Promise<Agent> {
|
|
7862
|
+
const key = \`\${actorId}:\${sessionId}\`;
|
|
7863
|
+
let agent = agentCache.get(key);
|
|
7864
|
+
if (agent) return agent;
|
|
7865
|
+
|
|
7866
|
+
const model = await loadModel();
|
|
7867
|
+
agent = new Agent({
|
|
7868
|
+
model,
|
|
7869
|
+
systemPrompt: SYSTEM_PROMPT,
|
|
7870
|
+
tools,
|
|
7871
|
+
memoryManager: getOrCreateMemoryManager(sessionId, actorId) ?? undefined,
|
|
7872
|
+
});
|
|
7873
|
+
agentCache.set(key, agent);
|
|
7874
|
+
return agent;
|
|
7875
|
+
}
|
|
7876
|
+
{{else}}
|
|
7877
|
+
const AGENT_CACHE_LIMIT = 128;
|
|
7878
|
+
|
|
7879
|
+
// Reuses one Agent per sessionId so each session keeps its own in-process
|
|
7880
|
+
// conversation history (best-effort; resets on cold start). A Map preserves
|
|
7881
|
+
// insertion order, so it doubles as an LRU bounded to 128 sessions — a local
|
|
7882
|
+
// dev process serving many sessions cannot leak history between them or grow
|
|
7883
|
+
// without bound. On AgentCore Runtime each microVM serves a single session, so
|
|
7884
|
+
// this holds one entry. For durable history, attach memory.
|
|
7885
|
+
const agentCache = new Map<string, Agent>();
|
|
7886
|
+
|
|
7887
|
+
async function getOrCreateAgent(sessionId: string): Promise<Agent> {
|
|
7888
|
+
const existing = agentCache.get(sessionId);
|
|
7889
|
+
if (existing) {
|
|
7890
|
+
agentCache.delete(sessionId);
|
|
7891
|
+
agentCache.set(sessionId, existing);
|
|
7892
|
+
return existing;
|
|
7893
|
+
}
|
|
7894
|
+
if (agentCache.size >= AGENT_CACHE_LIMIT) {
|
|
7895
|
+
const oldest = agentCache.keys().next().value;
|
|
7896
|
+
if (oldest !== undefined) agentCache.delete(oldest);
|
|
7897
|
+
}
|
|
7898
|
+
const model = await loadModel();
|
|
7899
|
+
const agent = new Agent({
|
|
7900
|
+
model,
|
|
7901
|
+
systemPrompt: SYSTEM_PROMPT,
|
|
7902
|
+
tools,
|
|
7903
|
+
});
|
|
7904
|
+
agentCache.set(sessionId, agent);
|
|
7905
|
+
return agent;
|
|
7906
|
+
}
|
|
7907
|
+
{{/if}}
|
|
7908
|
+
|
|
7909
|
+
const app = new BedrockAgentCoreApp({
|
|
7910
|
+
invocationHandler: {
|
|
7911
|
+
requestSchema,
|
|
7912
|
+
async *process(payload, context) {
|
|
7913
|
+
{{#if hasMemory}}
|
|
7914
|
+
const sessionId = context?.sessionId ?? 'default-session';
|
|
7915
|
+
const actorId = getActorId(payload, context);
|
|
7916
|
+
const agent = await getOrCreateAgent(sessionId, actorId);
|
|
7917
|
+
{{else}}
|
|
7918
|
+
const sessionId = context?.sessionId ?? 'default-session';
|
|
7919
|
+
const agent = await getOrCreateAgent(sessionId);
|
|
7920
|
+
{{/if}}
|
|
7921
|
+
|
|
7922
|
+
{{#if hasMemory}}
|
|
7923
|
+
try {
|
|
7924
|
+
for await (const event of agent.stream(payload.prompt)) {
|
|
7925
|
+
if (
|
|
7926
|
+
event.type === 'modelStreamUpdateEvent' &&
|
|
7927
|
+
event.event?.type === 'modelContentBlockDeltaEvent' &&
|
|
7928
|
+
event.event.delta?.type === 'textDelta'
|
|
7929
|
+
) {
|
|
7930
|
+
yield { data: event.event.delta.text };
|
|
7931
|
+
}
|
|
7932
|
+
}
|
|
7933
|
+
} finally {
|
|
7934
|
+
// Drain in-flight createEvent calls before the runtime can reclaim
|
|
7935
|
+
// the session microVM. flush() is the durability mechanism — without
|
|
7936
|
+
// it, an idle reclamation can lose the tail of the conversation.
|
|
7937
|
+
await agent.memoryManager?.flush();
|
|
7938
|
+
}
|
|
7939
|
+
{{else}}
|
|
7940
|
+
// Snapshot history before streaming so a failed turn can be rolled back.
|
|
7941
|
+
// Agent.stream() appends the user message before invoking the model; on a
|
|
7942
|
+
// mid-stream error that user turn would otherwise linger in the cached
|
|
7943
|
+
// agent, and the next turn for this session would send consecutive user
|
|
7944
|
+
// messages (rejected by providers that require strict role alternation,
|
|
7945
|
+
// e.g. Anthropic). Restoring on error keeps the session reusable.
|
|
7946
|
+
const snapshot = agent.takeSnapshot({ include: ['messages'] });
|
|
7947
|
+
try {
|
|
7948
|
+
for await (const event of agent.stream(payload.prompt)) {
|
|
7949
|
+
if (
|
|
7950
|
+
event.type === 'modelStreamUpdateEvent' &&
|
|
7951
|
+
event.event?.type === 'modelContentBlockDeltaEvent' &&
|
|
7952
|
+
event.event.delta?.type === 'textDelta'
|
|
7953
|
+
) {
|
|
7954
|
+
yield { data: event.event.delta.text };
|
|
7955
|
+
}
|
|
7956
|
+
}
|
|
7957
|
+
} catch (error) {
|
|
7958
|
+
agent.loadSnapshot(snapshot);
|
|
7959
|
+
throw error;
|
|
7960
|
+
}
|
|
7961
|
+
{{/if}}
|
|
7962
|
+
},
|
|
7963
|
+
},
|
|
7964
|
+
});
|
|
7965
|
+
|
|
7966
|
+
app.run({ port: parseInt(process.env.PORT ?? '8080') });
|
|
7967
|
+
"
|
|
7968
|
+
`;
|
|
7969
|
+
|
|
7970
|
+
exports[`Assets Directory Snapshots > TypeScript assets > typescript/typescript/http/strands/base/mcp_client/client.ts should match snapshot 1`] = `
|
|
7971
|
+
"import { McpClient } from '@strands-agents/sdk';
|
|
7972
|
+
import { StreamableHTTPClientTransport } from '@modelcontextprotocol/sdk/client/streamableHttp.js';
|
|
7973
|
+
|
|
7974
|
+
// ExaAI provides information about code through web searches, crawling and code context searches through their platform. Requires no authentication
|
|
7975
|
+
const EXAMPLE_MCP_ENDPOINT = 'https://mcp.exa.ai/mcp';
|
|
7976
|
+
|
|
7977
|
+
export function getStreamableHttpMcpClient(): McpClient {
|
|
7978
|
+
// to use an MCP server that supports bearer authentication, add a headers() callback to requestInit
|
|
7979
|
+
const transport = new StreamableHTTPClientTransport(new URL(EXAMPLE_MCP_ENDPOINT));
|
|
7980
|
+
return new McpClient({ transport });
|
|
7981
|
+
}
|
|
7982
|
+
"
|
|
7983
|
+
`;
|
|
7984
|
+
|
|
7985
|
+
exports[`Assets Directory Snapshots > TypeScript assets > typescript/typescript/http/strands/base/model/load.ts should match snapshot 1`] = `
|
|
7986
|
+
"{{#if (eq modelProvider "Bedrock")}}
|
|
7987
|
+
import { BedrockModel } from '@strands-agents/sdk/models/bedrock';
|
|
7988
|
+
|
|
7989
|
+
export function loadModel(): BedrockModel {
|
|
7990
|
+
return new BedrockModel({ modelId: 'global.anthropic.claude-sonnet-4-5-20250929-v1:0' });
|
|
7991
|
+
}
|
|
7992
|
+
{{/if}}
|
|
7993
|
+
{{#if (eq modelProvider "Anthropic")}}
|
|
7994
|
+
import { AnthropicModel } from '@strands-agents/sdk/models/anthropic';
|
|
7995
|
+
import { withApiKey } from 'bedrock-agentcore/identity';
|
|
7996
|
+
|
|
7997
|
+
const IDENTITY_PROVIDER_NAME = '{{identityProviders.[0].name}}';
|
|
7998
|
+
const IDENTITY_ENV_VAR = '{{identityProviders.[0].envVarName}}';
|
|
7999
|
+
|
|
8000
|
+
async function getApiKey(): Promise<string> {
|
|
8001
|
+
if (process.env.LOCAL_DEV === '1') {
|
|
8002
|
+
const apiKey = process.env[IDENTITY_ENV_VAR] ?? process.env.ANTHROPIC_API_KEY;
|
|
8003
|
+
if (!apiKey) {
|
|
8004
|
+
throw new Error(\`\${IDENTITY_ENV_VAR} or ANTHROPIC_API_KEY not found. Add your key to agentcore/.env.local\`);
|
|
8005
|
+
}
|
|
8006
|
+
return apiKey;
|
|
8007
|
+
}
|
|
8008
|
+
return withApiKey({ providerName: IDENTITY_PROVIDER_NAME })(async (apiKey: string) => apiKey)();
|
|
8009
|
+
}
|
|
8010
|
+
|
|
8011
|
+
let _model: AnthropicModel | undefined;
|
|
8012
|
+
|
|
8013
|
+
export async function loadModel(): Promise<AnthropicModel> {
|
|
8014
|
+
if (!_model) {
|
|
8015
|
+
const apiKey = await getApiKey();
|
|
8016
|
+
_model = new AnthropicModel({
|
|
8017
|
+
apiKey,
|
|
8018
|
+
modelId: 'claude-sonnet-4-5-20250929',
|
|
8019
|
+
maxTokens: 5000,
|
|
8020
|
+
});
|
|
8021
|
+
}
|
|
8022
|
+
return _model;
|
|
8023
|
+
}
|
|
8024
|
+
{{/if}}
|
|
8025
|
+
{{#if (eq modelProvider "OpenAI")}}
|
|
8026
|
+
import { OpenAIModel } from '@strands-agents/sdk/models/openai';
|
|
8027
|
+
import { withApiKey } from 'bedrock-agentcore/identity';
|
|
8028
|
+
|
|
8029
|
+
const IDENTITY_PROVIDER_NAME = '{{identityProviders.[0].name}}';
|
|
8030
|
+
const IDENTITY_ENV_VAR = '{{identityProviders.[0].envVarName}}';
|
|
8031
|
+
|
|
8032
|
+
async function getApiKey(): Promise<string> {
|
|
8033
|
+
if (process.env.LOCAL_DEV === '1') {
|
|
8034
|
+
const apiKey = process.env[IDENTITY_ENV_VAR] ?? process.env.OPENAI_API_KEY;
|
|
8035
|
+
if (!apiKey) {
|
|
8036
|
+
throw new Error(\`\${IDENTITY_ENV_VAR} or OPENAI_API_KEY not found. Add your key to agentcore/.env.local\`);
|
|
8037
|
+
}
|
|
8038
|
+
return apiKey;
|
|
8039
|
+
}
|
|
8040
|
+
return withApiKey({ providerName: IDENTITY_PROVIDER_NAME })(async (apiKey: string) => apiKey)();
|
|
8041
|
+
}
|
|
8042
|
+
|
|
8043
|
+
let _model: OpenAIModel | undefined;
|
|
8044
|
+
|
|
8045
|
+
export async function loadModel(): Promise<OpenAIModel> {
|
|
8046
|
+
if (!_model) {
|
|
8047
|
+
const apiKey = await getApiKey();
|
|
8048
|
+
_model = new OpenAIModel({
|
|
8049
|
+
api: 'chat',
|
|
8050
|
+
apiKey,
|
|
8051
|
+
modelId: 'gpt-4.1',
|
|
8052
|
+
});
|
|
8053
|
+
}
|
|
8054
|
+
return _model;
|
|
8055
|
+
}
|
|
8056
|
+
{{/if}}
|
|
8057
|
+
{{#if (eq modelProvider "Gemini")}}
|
|
8058
|
+
import { GoogleModel } from '@strands-agents/sdk/models/google';
|
|
8059
|
+
import { withApiKey } from 'bedrock-agentcore/identity';
|
|
8060
|
+
|
|
8061
|
+
const IDENTITY_PROVIDER_NAME = '{{identityProviders.[0].name}}';
|
|
8062
|
+
const IDENTITY_ENV_VAR = '{{identityProviders.[0].envVarName}}';
|
|
8063
|
+
|
|
8064
|
+
async function getApiKey(): Promise<string> {
|
|
8065
|
+
if (process.env.LOCAL_DEV === '1') {
|
|
8066
|
+
const apiKey = process.env[IDENTITY_ENV_VAR] ?? process.env.GEMINI_API_KEY;
|
|
8067
|
+
if (!apiKey) {
|
|
8068
|
+
throw new Error(\`\${IDENTITY_ENV_VAR} or GEMINI_API_KEY not found. Add your key to agentcore/.env.local\`);
|
|
8069
|
+
}
|
|
8070
|
+
return apiKey;
|
|
8071
|
+
}
|
|
8072
|
+
return withApiKey({ providerName: IDENTITY_PROVIDER_NAME })(async (apiKey: string) => apiKey)();
|
|
8073
|
+
}
|
|
8074
|
+
|
|
8075
|
+
let _model: GoogleModel | undefined;
|
|
8076
|
+
|
|
8077
|
+
export async function loadModel(): Promise<GoogleModel> {
|
|
8078
|
+
if (!_model) {
|
|
8079
|
+
const apiKey = await getApiKey();
|
|
8080
|
+
_model = new GoogleModel({
|
|
8081
|
+
apiKey,
|
|
8082
|
+
modelId: 'gemini-2.5-flash',
|
|
8083
|
+
});
|
|
8084
|
+
}
|
|
8085
|
+
return _model;
|
|
8086
|
+
}
|
|
8087
|
+
{{/if}}
|
|
8088
|
+
"
|
|
8089
|
+
`;
|
|
8090
|
+
|
|
8091
|
+
exports[`Assets Directory Snapshots > TypeScript assets > typescript/typescript/http/strands/base/package.json should match snapshot 1`] = `
|
|
8092
|
+
"{
|
|
8093
|
+
"name": "{{name}}",
|
|
8094
|
+
"version": "0.1.0",
|
|
8095
|
+
"description": "AgentCore Runtime Application using Strands TypeScript SDK",
|
|
8096
|
+
"private": true,
|
|
8097
|
+
"type": "module",
|
|
8098
|
+
"scripts": {
|
|
8099
|
+
"build": "tsc",
|
|
8100
|
+
"start": "node dist/main.js",
|
|
8101
|
+
"dev": "tsx watch main.ts"
|
|
8102
|
+
},
|
|
8103
|
+
"dependencies": {
|
|
8104
|
+
{{#if (eq modelProvider "Anthropic")}}
|
|
8105
|
+
"@anthropic-ai/sdk": "^0.71.2",
|
|
8106
|
+
{{/if}}
|
|
8107
|
+
{{#if (eq modelProvider "OpenAI")}}
|
|
8108
|
+
"openai": "^6.7.0",
|
|
8109
|
+
{{/if}}
|
|
8110
|
+
{{#if (eq modelProvider "Gemini")}}
|
|
8111
|
+
"@google/genai": "^1.40.0",
|
|
8112
|
+
{{/if}}
|
|
8113
|
+
"@modelcontextprotocol/sdk": "^1.25.2",
|
|
8114
|
+
"@opentelemetry/api": "^1.9.0",
|
|
8115
|
+
"@strands-agents/sdk": "^1.5.0",
|
|
8116
|
+
"bedrock-agentcore": "^0.3.0",
|
|
8117
|
+
"tsx": "^4.19.0",
|
|
8118
|
+
"zod": "^4.4.3"
|
|
8119
|
+
},
|
|
8120
|
+
"devDependencies": {
|
|
8121
|
+
"@types/node": "^22.0.0",
|
|
8122
|
+
"typescript": "^5.6.0"
|
|
8123
|
+
},
|
|
8124
|
+
"overrides": {
|
|
8125
|
+
"bedrock-agentcore": {
|
|
8126
|
+
"@strands-agents/sdk": "$@strands-agents/sdk"
|
|
8127
|
+
}
|
|
8128
|
+
}
|
|
8129
|
+
}
|
|
8130
|
+
"
|
|
8131
|
+
`;
|
|
8132
|
+
|
|
8133
|
+
exports[`Assets Directory Snapshots > TypeScript assets > typescript/typescript/http/strands/base/tsconfig.json should match snapshot 1`] = `
|
|
8134
|
+
"{
|
|
8135
|
+
"compilerOptions": {
|
|
8136
|
+
"target": "ES2022",
|
|
8137
|
+
"module": "NodeNext",
|
|
8138
|
+
"moduleResolution": "NodeNext",
|
|
8139
|
+
"strict": true,
|
|
8140
|
+
"esModuleInterop": true,
|
|
8141
|
+
"skipLibCheck": true,
|
|
8142
|
+
"forceConsistentCasingInFileNames": true,
|
|
8143
|
+
"resolveJsonModule": true,
|
|
8144
|
+
"declaration": false,
|
|
8145
|
+
"sourceMap": true,
|
|
8146
|
+
"outDir": "dist",
|
|
8147
|
+
"rootDir": ".",
|
|
8148
|
+
"types": ["node"]
|
|
8149
|
+
},
|
|
8150
|
+
"include": ["**/*.ts"],
|
|
8151
|
+
"exclude": ["node_modules", "dist"]
|
|
8152
|
+
}
|
|
8153
|
+
"
|
|
8154
|
+
`;
|
|
8155
|
+
|
|
8156
|
+
exports[`Assets Directory Snapshots > TypeScript assets > typescript/typescript/http/strands/capabilities/memory/memory.ts should match snapshot 1`] = `
|
|
8157
|
+
"import { randomUUID } from 'node:crypto';
|
|
8158
|
+
import { MemoryManager } from '@strands-agents/sdk';
|
|
8159
|
+
import { createAgentCoreMemoryStores } from 'bedrock-agentcore/experimental/memory/strands';
|
|
8160
|
+
|
|
8161
|
+
const MEMORY_ID = process.env.{{memoryProviders.[0].envVarName}};
|
|
8162
|
+
|
|
8163
|
+
const CUSTOM_ACTOR_ID_HEADER = 'x-amzn-bedrock-agentcore-runtime-custom-actor-id';
|
|
8164
|
+
|
|
8165
|
+
export function getActorId(payload: any, context: any): string {
|
|
8166
|
+
const raw =
|
|
8167
|
+
context?.headers?.[CUSTOM_ACTOR_ID_HEADER] ||
|
|
8168
|
+
payload?.userId ||
|
|
8169
|
+
context?.sessionId;
|
|
8170
|
+
return typeof raw === 'string' && raw.trim().length > 0 ? raw.trim() : randomUUID();
|
|
8171
|
+
}
|
|
8172
|
+
|
|
8173
|
+
const memoryManagerCache = new Map<string, MemoryManager>();
|
|
8174
|
+
|
|
8175
|
+
export function getOrCreateMemoryManager(sessionId: string, actorId: string): MemoryManager | null {
|
|
8176
|
+
if (!MEMORY_ID) return null;
|
|
8177
|
+
|
|
8178
|
+
const key = \`\${actorId}:\${sessionId}\`;
|
|
8179
|
+
let manager = memoryManagerCache.get(key);
|
|
8180
|
+
if (manager) return manager;
|
|
8181
|
+
|
|
8182
|
+
const stores = createAgentCoreMemoryStores({
|
|
8183
|
+
memoryId: MEMORY_ID,
|
|
8184
|
+
actorId,
|
|
8185
|
+
sessionId,
|
|
8186
|
+
namespaces: [
|
|
8187
|
+
{{#if (includes memoryProviders.[0].strategies "SEMANTIC")}}
|
|
8188
|
+
{ namespace: '/users/{actorId}/facts' },
|
|
8189
|
+
{{/if}}
|
|
8190
|
+
{{#if (includes memoryProviders.[0].strategies "USER_PREFERENCE")}}
|
|
8191
|
+
{ namespace: '/users/{actorId}/preferences' },
|
|
8192
|
+
{{/if}}
|
|
8193
|
+
{{#if (includes memoryProviders.[0].strategies "EPISODIC")}}
|
|
8194
|
+
{ namespace: '/episodes/{actorId}/{sessionId}' },
|
|
8195
|
+
{{/if}}
|
|
8196
|
+
{{#if (includes memoryProviders.[0].strategies "SUMMARIZATION")}}
|
|
8197
|
+
{ namespace: '/summaries/{actorId}/{sessionId}' },
|
|
8198
|
+
{{/if}}
|
|
8199
|
+
],
|
|
8200
|
+
// readMode defaults to 'per-namespace' (one retrieve call per namespace).
|
|
8201
|
+
// Switch to 'subtree' to consolidate to a single hierarchical recall call.
|
|
8202
|
+
extraction: true,
|
|
8203
|
+
});
|
|
8204
|
+
|
|
8205
|
+
manager = new MemoryManager({ stores });
|
|
8206
|
+
memoryManagerCache.set(key, manager);
|
|
8207
|
+
return manager;
|
|
8208
|
+
}
|
|
8209
|
+
"
|
|
8210
|
+
`;
|
|
8211
|
+
|
|
8212
|
+
exports[`Assets Directory Snapshots > TypeScript assets > typescript/typescript/http/vercelai/base/README.md should match snapshot 1`] = `
|
|
8213
|
+
"This is a project generated by the AgentCore CLI!
|
|
8214
|
+
|
|
8215
|
+
# Layout
|
|
8216
|
+
|
|
8217
|
+
The generated application code lives at the agent root directory. At the root, there is a \`.gitignore\` file, an
|
|
8218
|
+
\`agentcore/\` folder which represents the configurations and state associated with this project. Other \`agentcore\`
|
|
8219
|
+
commands like \`deploy\`, \`dev\`, and \`invoke\` rely on the configuration stored here.
|
|
8220
|
+
|
|
8221
|
+
## Agent Root
|
|
8222
|
+
|
|
8223
|
+
The main entrypoint to your app is defined in \`main.ts\`. Using the AgentCore SDK \`BedrockAgentCoreApp\`, this file
|
|
8224
|
+
defines an HTTP app that streams tokens using the Vercel AI SDK's \`streamText\` API.
|
|
8225
|
+
|
|
8226
|
+
\`model/load.ts\` instantiates your chosen model provider.
|
|
8227
|
+
|
|
8228
|
+
## Input Validation
|
|
8229
|
+
|
|
8230
|
+
The generated Zod request schema keeps plain prompts typed as strings before forwarding them to the agent framework.
|
|
8231
|
+
Retain this validation when extending the request shape, and pass only prompt text to the agent.
|
|
8232
|
+
|
|
8233
|
+
## Environment Variables
|
|
8234
|
+
|
|
8235
|
+
| Variable | Required | Description |
|
|
8236
|
+
| --- | --- | --- |
|
|
8237
|
+
{{#if hasIdentity}}| \`{{identityProviders.[0].envVarName}}\` | Yes | {{modelProvider}} API key (local) or Identity provider name (deployed) |
|
|
8238
|
+
{{/if}}| \`LOCAL_DEV\` | No | Set to \`1\` to use \`.env.local\` instead of AgentCore Identity |
|
|
8239
|
+
|
|
8240
|
+
# Developing locally
|
|
8241
|
+
|
|
8242
|
+
If installation was successful, \`node_modules/\` is already populated with dependencies.
|
|
8243
|
+
|
|
8244
|
+
\`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\`).
|
|
8245
|
+
|
|
8246
|
+
In a new terminal, you can invoke that server with:
|
|
8247
|
+
|
|
8248
|
+
\`agentcore invoke --dev "What can you do"\`
|
|
8249
|
+
|
|
8250
|
+
# Deployment
|
|
8251
|
+
|
|
8252
|
+
After providing credentials, \`agentcore deploy\` will deploy your project into Amazon Bedrock AgentCore.
|
|
8253
|
+
|
|
8254
|
+
Use \`agentcore invoke\` to invoke your deployed agent.
|
|
8255
|
+
"
|
|
8256
|
+
`;
|
|
8257
|
+
|
|
8258
|
+
exports[`Assets Directory Snapshots > TypeScript assets > typescript/typescript/http/vercelai/base/gitignore.template should match snapshot 1`] = `
|
|
8259
|
+
"# Environment variables
|
|
8260
|
+
.env
|
|
8261
|
+
.env.*
|
|
8262
|
+
|
|
8263
|
+
# Node
|
|
8264
|
+
node_modules/
|
|
8265
|
+
dist/
|
|
8266
|
+
*.log
|
|
8267
|
+
npm-debug.log*
|
|
8268
|
+
yarn-debug.log*
|
|
8269
|
+
yarn-error.log*
|
|
8270
|
+
|
|
8271
|
+
# IDE
|
|
8272
|
+
.vscode/
|
|
8273
|
+
.idea/
|
|
8274
|
+
*.swp
|
|
8275
|
+
*.swo
|
|
8276
|
+
*~
|
|
8277
|
+
|
|
8278
|
+
# OS
|
|
8279
|
+
.DS_Store
|
|
8280
|
+
Thumbs.db
|
|
8281
|
+
"
|
|
8282
|
+
`;
|
|
8283
|
+
|
|
8284
|
+
exports[`Assets Directory Snapshots > TypeScript assets > typescript/typescript/http/vercelai/base/main.ts should match snapshot 1`] = `
|
|
8285
|
+
"import { BedrockAgentCoreApp } from 'bedrock-agentcore/runtime';
|
|
8286
|
+
import { streamText, type ModelMessage } from 'ai';
|
|
8287
|
+
import { z } from 'zod';
|
|
8288
|
+
import { loadModel } from './model/load.js';
|
|
8289
|
+
|
|
8290
|
+
const SYSTEM_PROMPT = \`You are a helpful assistant.\`;
|
|
8291
|
+
|
|
8292
|
+
const requestSchema = z.object({
|
|
8293
|
+
prompt: z.string().default(''),
|
|
8294
|
+
});
|
|
8295
|
+
|
|
8296
|
+
const HISTORY_LIMIT = 128;
|
|
8297
|
+
|
|
8298
|
+
// Keeps one message history per sessionId so each session remembers its own
|
|
8299
|
+
// turns (best-effort; resets on cold start). A Map preserves insertion order,
|
|
8300
|
+
// so it doubles as an LRU bounded to 128 sessions — a local dev process serving
|
|
8301
|
+
// many sessions cannot leak history between them or grow without bound. On
|
|
8302
|
+
// AgentCore Runtime each microVM serves a single session, so this holds one
|
|
8303
|
+
// entry. For durable history, persist messages to an external store.
|
|
8304
|
+
const histories = new Map<string, ModelMessage[]>();
|
|
8305
|
+
|
|
8306
|
+
function getHistory(sessionId: string): ModelMessage[] {
|
|
8307
|
+
const existing = histories.get(sessionId);
|
|
8308
|
+
if (existing) {
|
|
8309
|
+
histories.delete(sessionId);
|
|
8310
|
+
histories.set(sessionId, existing);
|
|
8311
|
+
return existing;
|
|
8312
|
+
}
|
|
8313
|
+
if (histories.size >= HISTORY_LIMIT) {
|
|
8314
|
+
const oldest = histories.keys().next().value;
|
|
8315
|
+
if (oldest !== undefined) histories.delete(oldest);
|
|
8316
|
+
}
|
|
8317
|
+
const fresh: ModelMessage[] = [];
|
|
8318
|
+
histories.set(sessionId, fresh);
|
|
8319
|
+
return fresh;
|
|
8320
|
+
}
|
|
8321
|
+
|
|
8322
|
+
const app = new BedrockAgentCoreApp({
|
|
8323
|
+
invocationHandler: {
|
|
8324
|
+
requestSchema,
|
|
8325
|
+
async *process(payload, context) {
|
|
8326
|
+
const sessionId = context?.sessionId ?? 'default-session';
|
|
8327
|
+
const history = getHistory(sessionId);
|
|
8328
|
+
const userMessage: ModelMessage = { role: 'user', content: payload.prompt };
|
|
8329
|
+
|
|
8330
|
+
const model = await loadModel();
|
|
8331
|
+
const result = streamText({
|
|
8332
|
+
model,
|
|
8333
|
+
system: SYSTEM_PROMPT,
|
|
8334
|
+
messages: [...history, userMessage],
|
|
8335
|
+
});
|
|
8336
|
+
|
|
8337
|
+
let assistant = '';
|
|
8338
|
+
for await (const chunk of result.textStream) {
|
|
8339
|
+
assistant += chunk;
|
|
8340
|
+
yield { data: chunk };
|
|
8341
|
+
}
|
|
8342
|
+
|
|
8343
|
+
// Commit the exchange to history only after a non-empty reply. On a failed
|
|
8344
|
+
// or empty stream the turn is dropped instead of leaving a dangling user
|
|
8345
|
+
// (or empty assistant) message — consecutive same-role or empty-content
|
|
8346
|
+
// messages would otherwise be rejected on the next turn for this session.
|
|
8347
|
+
if (assistant.length > 0) {
|
|
8348
|
+
history.push(userMessage, { role: 'assistant', content: assistant });
|
|
8349
|
+
}
|
|
8350
|
+
},
|
|
8351
|
+
},
|
|
8352
|
+
});
|
|
8353
|
+
|
|
8354
|
+
app.run({ port: parseInt(process.env.PORT ?? '8080') });
|
|
8355
|
+
"
|
|
8356
|
+
`;
|
|
8357
|
+
|
|
8358
|
+
exports[`Assets Directory Snapshots > TypeScript assets > typescript/typescript/http/vercelai/base/model/load.ts should match snapshot 1`] = `
|
|
8359
|
+
"{{#if (eq modelProvider "Bedrock")}}
|
|
8360
|
+
import { createAmazonBedrock } from '@ai-sdk/amazon-bedrock';
|
|
8361
|
+
import { fromNodeProviderChain } from '@aws-sdk/credential-providers';
|
|
8362
|
+
|
|
8363
|
+
const provider = fromNodeProviderChain();
|
|
8364
|
+
|
|
8365
|
+
const bedrock = createAmazonBedrock({
|
|
8366
|
+
region: process.env.AWS_REGION ?? 'us-east-1',
|
|
8367
|
+
credentialProvider: async () => {
|
|
8368
|
+
const creds = await provider();
|
|
8369
|
+
return {
|
|
8370
|
+
accessKeyId: creds.accessKeyId,
|
|
8371
|
+
secretAccessKey: creds.secretAccessKey,
|
|
8372
|
+
sessionToken: creds.sessionToken,
|
|
8373
|
+
};
|
|
8374
|
+
},
|
|
8375
|
+
});
|
|
8376
|
+
|
|
8377
|
+
export function loadModel() {
|
|
8378
|
+
return bedrock('us.anthropic.claude-sonnet-4-5-20250929-v1:0');
|
|
8379
|
+
}
|
|
8380
|
+
{{/if}}
|
|
8381
|
+
{{#if (eq modelProvider "Anthropic")}}
|
|
8382
|
+
import { createAnthropic } from '@ai-sdk/anthropic';
|
|
8383
|
+
import { withApiKey } from 'bedrock-agentcore/identity';
|
|
8384
|
+
|
|
8385
|
+
const IDENTITY_PROVIDER_NAME = '{{identityProviders.[0].name}}';
|
|
8386
|
+
const IDENTITY_ENV_VAR = '{{identityProviders.[0].envVarName}}';
|
|
8387
|
+
|
|
8388
|
+
async function getApiKey(): Promise<string> {
|
|
8389
|
+
if (process.env.LOCAL_DEV === '1') {
|
|
8390
|
+
const apiKey = process.env[IDENTITY_ENV_VAR] ?? process.env.ANTHROPIC_API_KEY;
|
|
8391
|
+
if (!apiKey) {
|
|
8392
|
+
throw new Error(\`\${IDENTITY_ENV_VAR} or ANTHROPIC_API_KEY not found. Add your key to agentcore/.env.local\`);
|
|
8393
|
+
}
|
|
8394
|
+
return apiKey;
|
|
8395
|
+
}
|
|
8396
|
+
return withApiKey({ providerName: IDENTITY_PROVIDER_NAME })(async (apiKey: string) => apiKey)();
|
|
8397
|
+
}
|
|
8398
|
+
|
|
8399
|
+
let _anthropic: ReturnType<typeof createAnthropic> | undefined;
|
|
8400
|
+
|
|
8401
|
+
async function getProvider() {
|
|
8402
|
+
if (!_anthropic) {
|
|
8403
|
+
const apiKey = await getApiKey();
|
|
8404
|
+
_anthropic = createAnthropic({ apiKey });
|
|
8405
|
+
}
|
|
8406
|
+
return _anthropic;
|
|
8407
|
+
}
|
|
8408
|
+
|
|
8409
|
+
export async function loadModel() {
|
|
8410
|
+
const anthropic = await getProvider();
|
|
8411
|
+
return anthropic('claude-sonnet-4-5-20250929');
|
|
8412
|
+
}
|
|
8413
|
+
{{/if}}
|
|
8414
|
+
{{#if (eq modelProvider "OpenAI")}}
|
|
8415
|
+
import { createOpenAI } from '@ai-sdk/openai';
|
|
8416
|
+
import { withApiKey } from 'bedrock-agentcore/identity';
|
|
8417
|
+
|
|
8418
|
+
const IDENTITY_PROVIDER_NAME = '{{identityProviders.[0].name}}';
|
|
8419
|
+
const IDENTITY_ENV_VAR = '{{identityProviders.[0].envVarName}}';
|
|
8420
|
+
|
|
8421
|
+
async function getApiKey(): Promise<string> {
|
|
8422
|
+
if (process.env.LOCAL_DEV === '1') {
|
|
8423
|
+
const apiKey = process.env[IDENTITY_ENV_VAR] ?? process.env.OPENAI_API_KEY;
|
|
8424
|
+
if (!apiKey) {
|
|
8425
|
+
throw new Error(\`\${IDENTITY_ENV_VAR} or OPENAI_API_KEY not found. Add your key to agentcore/.env.local\`);
|
|
8426
|
+
}
|
|
8427
|
+
return apiKey;
|
|
8428
|
+
}
|
|
8429
|
+
return withApiKey({ providerName: IDENTITY_PROVIDER_NAME })(async (apiKey: string) => apiKey)();
|
|
8430
|
+
}
|
|
8431
|
+
|
|
8432
|
+
let _openai: ReturnType<typeof createOpenAI> | undefined;
|
|
8433
|
+
|
|
8434
|
+
async function getProvider() {
|
|
8435
|
+
if (!_openai) {
|
|
8436
|
+
const apiKey = await getApiKey();
|
|
8437
|
+
_openai = createOpenAI({ apiKey });
|
|
8438
|
+
}
|
|
8439
|
+
return _openai;
|
|
8440
|
+
}
|
|
8441
|
+
|
|
8442
|
+
export async function loadModel() {
|
|
8443
|
+
const openai = await getProvider();
|
|
8444
|
+
return openai('gpt-4.1');
|
|
8445
|
+
}
|
|
8446
|
+
{{/if}}
|
|
8447
|
+
{{#if (eq modelProvider "Gemini")}}
|
|
8448
|
+
import { createGoogleGenerativeAI } from '@ai-sdk/google';
|
|
8449
|
+
import { withApiKey } from 'bedrock-agentcore/identity';
|
|
8450
|
+
|
|
8451
|
+
const IDENTITY_PROVIDER_NAME = '{{identityProviders.[0].name}}';
|
|
8452
|
+
const IDENTITY_ENV_VAR = '{{identityProviders.[0].envVarName}}';
|
|
8453
|
+
|
|
8454
|
+
async function getApiKey(): Promise<string> {
|
|
8455
|
+
if (process.env.LOCAL_DEV === '1') {
|
|
8456
|
+
const apiKey = process.env[IDENTITY_ENV_VAR] ?? process.env.GEMINI_API_KEY;
|
|
8457
|
+
if (!apiKey) {
|
|
8458
|
+
throw new Error(\`\${IDENTITY_ENV_VAR} or GEMINI_API_KEY not found. Add your key to agentcore/.env.local\`);
|
|
8459
|
+
}
|
|
8460
|
+
return apiKey;
|
|
8461
|
+
}
|
|
8462
|
+
return withApiKey({ providerName: IDENTITY_PROVIDER_NAME })(async (apiKey: string) => apiKey)();
|
|
8463
|
+
}
|
|
8464
|
+
|
|
8465
|
+
let _google: ReturnType<typeof createGoogleGenerativeAI> | undefined;
|
|
8466
|
+
|
|
8467
|
+
async function getProvider() {
|
|
8468
|
+
if (!_google) {
|
|
8469
|
+
const apiKey = await getApiKey();
|
|
8470
|
+
_google = createGoogleGenerativeAI({ apiKey });
|
|
8471
|
+
}
|
|
8472
|
+
return _google;
|
|
8473
|
+
}
|
|
8474
|
+
|
|
8475
|
+
export async function loadModel() {
|
|
8476
|
+
const google = await getProvider();
|
|
8477
|
+
return google('gemini-2.5-flash');
|
|
8478
|
+
}
|
|
8479
|
+
{{/if}}
|
|
8480
|
+
"
|
|
8481
|
+
`;
|
|
8482
|
+
|
|
8483
|
+
exports[`Assets Directory Snapshots > TypeScript assets > typescript/typescript/http/vercelai/base/package.json should match snapshot 1`] = `
|
|
8484
|
+
"{
|
|
8485
|
+
"name": "{{name}}",
|
|
8486
|
+
"version": "0.1.0",
|
|
8487
|
+
"description": "AgentCore Runtime Application using Vercel AI SDK",
|
|
8488
|
+
"private": true,
|
|
8489
|
+
"type": "module",
|
|
8490
|
+
"scripts": {
|
|
8491
|
+
"build": "tsc",
|
|
8492
|
+
"start": "node dist/main.js",
|
|
8493
|
+
"dev": "tsx watch main.ts"
|
|
8494
|
+
},
|
|
8495
|
+
"dependencies": {
|
|
8496
|
+
"ai": "^6.0.0",
|
|
8497
|
+
{{#if (eq modelProvider "Bedrock")}}
|
|
8498
|
+
"@ai-sdk/amazon-bedrock": "^4.0.0",
|
|
8499
|
+
"@aws-sdk/credential-providers": "^3.0.0",
|
|
8500
|
+
{{/if}}
|
|
8501
|
+
{{#if (eq modelProvider "Anthropic")}}
|
|
8502
|
+
"@ai-sdk/anthropic": "^3.0.0",
|
|
8503
|
+
{{/if}}
|
|
8504
|
+
{{#if (eq modelProvider "OpenAI")}}
|
|
8505
|
+
"@ai-sdk/openai": "^3.0.0",
|
|
8506
|
+
{{/if}}
|
|
8507
|
+
{{#if (eq modelProvider "Gemini")}}
|
|
8508
|
+
"@ai-sdk/google": "^3.0.0",
|
|
8509
|
+
{{/if}}
|
|
8510
|
+
"bedrock-agentcore": "^0.2.4",
|
|
8511
|
+
"tsx": "^4.19.0",
|
|
8512
|
+
"zod": "^3.24.0"
|
|
8513
|
+
},
|
|
8514
|
+
"devDependencies": {
|
|
8515
|
+
"@types/node": "^22.0.0",
|
|
8516
|
+
"typescript": "^5.6.0"
|
|
8517
|
+
}
|
|
8518
|
+
}
|
|
8519
|
+
"
|
|
8520
|
+
`;
|
|
8521
|
+
|
|
8522
|
+
exports[`Assets Directory Snapshots > TypeScript assets > typescript/typescript/http/vercelai/base/tsconfig.json should match snapshot 1`] = `
|
|
8523
|
+
"{
|
|
8524
|
+
"compilerOptions": {
|
|
8525
|
+
"target": "ES2022",
|
|
8526
|
+
"module": "NodeNext",
|
|
8527
|
+
"moduleResolution": "NodeNext",
|
|
8528
|
+
"strict": true,
|
|
8529
|
+
"esModuleInterop": true,
|
|
8530
|
+
"skipLibCheck": true,
|
|
8531
|
+
"forceConsistentCasingInFileNames": true,
|
|
8532
|
+
"resolveJsonModule": true,
|
|
8533
|
+
"declaration": false,
|
|
8534
|
+
"sourceMap": true,
|
|
8535
|
+
"outDir": "dist",
|
|
8536
|
+
"rootDir": ".",
|
|
8537
|
+
"types": ["node"]
|
|
8538
|
+
},
|
|
8539
|
+
"include": ["**/*.ts"],
|
|
8540
|
+
"exclude": ["node_modules", "dist"]
|
|
8541
|
+
}
|
|
8542
|
+
"
|
|
8543
|
+
`;
|